Skip to content

Commit

Permalink
Ignore the query cache in a block
Browse files Browse the repository at this point in the history
Add the ability to ignore the query cache in a block. In the block,
- queries will not be cached
- queries will not be read from the cache
- writes will not clear the cache
- cached/uncached blocks will be ignored

This is something we need for Solid Cache
(see rails/solid_cache#123).

As Solid Cache uses the database it gets the automatic query cache
behaviour by default. But it would be better if it ignored the query
cache entirely.

The local cache already handles caching repeated reads and having cache
writes clear the AR query cache is not desirable.

`uncached` works for disabling reads doesn't disable clearing the cache
for writes.
  • Loading branch information
djmb committed Jan 10, 2024
1 parent eac48e9 commit d6218e2
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 3 deletions.
17 changes: 17 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
* Add `ActiveRecord::ConnectionAdapters::QueryCache#ignore_query_cache`

This entirely ignores the query cache within a block.

- Queries are not cached or read from the cache
- Writes do not clear the cache
- cached/uncached blocks do nothing

```ruby
ActiveRecord::Base.connection.ignore_query_cache do
post = Post.find 1
post.touch
end
```

*Donal McBreen*

* Make `ActiveRecord::Encryption::Encryptor` agnostic of the serialization format used for encrypted data.

Previously, the encryptor instance only allowed an encrypted value serialized as a `String` to be passed to the message serializer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
def #{method_name}(...)
ActiveRecord::Base.clear_query_caches_for_current_thread
ActiveRecord::Base.clear_query_caches_for_current_thread unless @query_cache_ignored
super
end
end_code
Expand Down Expand Up @@ -50,12 +50,13 @@ def query_cache_enabled
end
end

attr_reader :query_cache, :query_cache_enabled
attr_reader :query_cache, :query_cache_enabled, :query_cache_ignored

def initialize(*)
super
@query_cache = {}
@query_cache_enabled = false
@query_cache_ignored = false
@query_cache_max_size = nil
end

Expand Down Expand Up @@ -85,6 +86,13 @@ def uncached
@query_cache_enabled = old
end

def ignore_query_cache
old, @query_cache_ignored = @query_cache_ignored, true
yield
ensure
@query_cache_ignored = old
end

# Clears the query cache.
#
# One reason you may wish to call this method explicitly is between queries
Expand All @@ -102,7 +110,7 @@ def select_all(arel, name = nil, binds = [], preparable: nil, async: false) # :n

# If arel is locked this is a SELECT ... FOR UPDATE or somesuch.
# Such queries should not be cached.
if @query_cache_enabled && !(arel.respond_to?(:locked) && arel.locked)
if @query_cache_enabled && !@query_cache_ignored && !(arel.respond_to?(:locked) && arel.locked)
sql, binds, preparable = to_sql_and_binds(arel, binds, preparable)

if async
Expand Down
36 changes: 36 additions & 0 deletions activerecord/test/cases/query_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,42 @@ def test_clear_query_cache_is_called_on_all_connections
ActiveRecord::Base.connection_pool.lock_thread = false
end

def test_the_query_cache_is_not_used_when_it_is_ignored
middleware do |env|
Task.find 1

assert_queries(1) do
Task.connection.ignore_query_cache { Task.find 1 }
end
end
end

def test_queries_are_not_cached_when_the_query_cache_is_ignored
middleware do |env|
assert_equal false, Task.connection.query_cache_ignored
Task.connection.ignore_query_cache do
assert_equal true, Task.connection.query_cache_ignored
Task.find 1
end
query_cache = ActiveRecord::Base.connection.query_cache
assert_equal 0, query_cache.length, query_cache.keys
end
end

def test_writes_do_not_clear_the_query_cache_when_it_is_ignored
middleware do |env|
query_cache = ActiveRecord::Base.connection.query_cache

Task.find 1
assert_equal 1, query_cache.length, query_cache.keys

Task.connection.ignore_query_cache { Task.create! }
assert_equal 1, query_cache.length, query_cache.keys

assert_no_queries { Task.find 1 }
end
end

private
def with_temporary_connection_pool(&block)
pool_config = ActiveRecord::Base.connection.pool.pool_config
Expand Down

0 comments on commit d6218e2

Please sign in to comment.