Add dirties option to uncached (#51204)

This adds a `dirties` option to `ActiveRecord::Base.uncached` and
`ActiveRecord::ConnectionAdapters::ConnectionPool#uncached`.

Setting `dirties` to `false`, means database writes to the connection
pool will not mark any query caches as dirty.

The option defaults to `true` which retains the existing behaviour and
clears query caches on all connection pools used by the current thread.

Co-authored-by: Jeremy Daer <jeremy@rubyonrails.org>
This commit is contained in:
Donal McBreen 2024-03-03 03:01:24 +00:00 committed by GitHub
parent 5cedb8745c
commit 5d528ba0c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 13 deletions

View File

@ -1,3 +1,15 @@
* Add dirties option to uncached
This adds a `dirties` option to `ActiveRecord::Base.uncached` and
`ActiveRecord::ConnectionAdapters::ConnectionPool#uncached`.
When set to `true` (the default), writes will clear all query caches belonging to the current thread.
When set to `false`, writes to the affected connection pool will not clear any query cache.
This is needed by Solid Cache so that cache writes do not clear query caches.
*Donal McBreen*
* Deprecate `ActiveRecord::Base.connection` in favor of `.lease_connection`
The method has been renamed as `lease_connection` to better reflect that the returned

View File

@ -41,9 +41,14 @@ module ActiveRecord
def checkin(_); end
def remove(_); end
def async_executor; end
def db_config
NULL_CONFIG
end
def dirties_query_cache
true
end
end
# = Active Record Connection Pool

View File

@ -20,7 +20,9 @@ module ActiveRecord
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
if pool.dirties_query_cache
ActiveRecord::Base.clear_query_caches_for_current_thread
end
super
end
end_code
@ -29,13 +31,15 @@ module ActiveRecord
end
class Store # :nodoc:
attr_accessor :enabled
attr_accessor :enabled, :dirties
alias_method :enabled?, :enabled
alias_method :dirties?, :dirties
def initialize(max_size)
@map = {}
@max_size = max_size
@enabled = false
@dirties = true
end
def size
@ -96,38 +100,42 @@ module ActiveRecord
end
# Disable the query cache within the block.
def disable_query_cache
def disable_query_cache(dirties: true)
cache = query_cache
old, cache.enabled = cache.enabled, false
old_enabled, cache.enabled, old_dirties, cache.dirties = cache.enabled, false, cache.dirties, dirties
begin
yield
ensure
cache.enabled = old
cache.enabled, cache.dirties = old_enabled, old_dirties
end
end
def enable_query_cache
cache = query_cache
old, cache.enabled = cache.enabled, true
old_enabled, cache.enabled, old_dirties, cache.dirties = cache.enabled, true, cache.dirties, true
begin
yield
ensure
cache.enabled = old
cache.enabled, cache.dirties = old_enabled, old_dirties
end
end
def enable_query_cache!
query_cache.enabled = true
query_cache.enabled, query_cache.dirties = true, true
end
def disable_query_cache!
query_cache.enabled = false
query_cache.enabled, query_cache.dirties = false, true
end
def query_cache_enabled
query_cache.enabled
end
def dirties_query_cache
query_cache.dirties
end
def clear_query_cache
if @pinned_connection
# With transactional fixtures, and especially systems test
@ -175,8 +183,11 @@ module ActiveRecord
end
# Disable the query cache within the block.
def uncached(&)
pool.disable_query_cache(&)
#
# Set <tt>dirties: false</tt> to prevent query caches on all connections from being cleared by write operations.
# (By default, write operations dirty all connections' query caches in case they are replicas whose cache would now be outdated.)
def uncached(dirties: true, &)
pool.disable_query_cache(dirties: dirties, &)
end
def disable_query_cache!

View File

@ -22,9 +22,12 @@ module ActiveRecord
# Disable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def uncached(&block)
#
# Set <tt>dirties: false</tt> to prevent query caches on all connections from being cleared by write operations.
# (By default, write operations dirty all connections' query caches in case they are replicas whose cache would now be outdated.)
def uncached(dirties: true, &block)
if connected? || !configurations.empty?
connection_pool.disable_query_cache(&block)
connection_pool.disable_query_cache(dirties: dirties, &block)
else
yield
end

View File

@ -706,6 +706,57 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
def test_query_cache_uncached_dirties
mw = middleware { |env|
Post.first
assert_no_changes -> { ActiveRecord::Base.connection.query_cache.size } do
Post.uncached(dirties: false) { Post.create!(title: "a new post", body: "and a body") }
end
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
Post.uncached(dirties: true) { Post.create!(title: "a new post", body: "and a body") }
end
}
mw.call({})
end
def test_query_cache_connection_uncached_dirties
mw = middleware { |env|
Post.first
assert_no_changes -> { ActiveRecord::Base.connection.query_cache.size } do
Post.connection.uncached(dirties: false) { Post.create!(title: "a new post", body: "and a body") }
end
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
Post.connection.uncached(dirties: true) { Post.create!(title: "a new post", body: "and a body") }
end
}
mw.call({})
end
def test_query_cache_uncached_dirties_disabled_with_nested_cache
mw = middleware { |env|
Post.first
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
Post.uncached(dirties: false) do
Post.cache do
Post.create!(title: "a new post", body: "and a body")
end
end
end
Post.first
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
Post.connection.uncached(dirties: false) do
Post.connection.cache do
Post.create!(title: "a new post", body: "and a body")
end
end
end
}
mw.call({})
end
private
def with_temporary_connection_pool(&block)
pool_config = ActiveRecord::Base.lease_connection.pool.pool_config