Skip to content

Commit 5d528ba

Browse files
djmbjeremy
andauthored
Add dirties option to uncached (rails#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 <[email protected]>
1 parent 5cedb87 commit 5d528ba

File tree

5 files changed

+95
-13
lines changed

5 files changed

+95
-13
lines changed

activerecord/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
* Add dirties option to uncached
2+
3+
This adds a `dirties` option to `ActiveRecord::Base.uncached` and
4+
`ActiveRecord::ConnectionAdapters::ConnectionPool#uncached`.
5+
6+
When set to `true` (the default), writes will clear all query caches belonging to the current thread.
7+
When set to `false`, writes to the affected connection pool will not clear any query cache.
8+
9+
This is needed by Solid Cache so that cache writes do not clear query caches.
10+
11+
*Donal McBreen*
12+
113
* Deprecate `ActiveRecord::Base.connection` in favor of `.lease_connection`
214

315
The method has been renamed as `lease_connection` to better reflect that the returned

activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@ def connection_class; end
4141
def checkin(_); end
4242
def remove(_); end
4343
def async_executor; end
44+
4445
def db_config
4546
NULL_CONFIG
4647
end
48+
49+
def dirties_query_cache
50+
true
51+
end
4752
end
4853

4954
# = Active Record Connection Pool

activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ def dirties_query_cache(base, *method_names)
2020
method_names.each do |method_name|
2121
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
2222
def #{method_name}(...)
23-
ActiveRecord::Base.clear_query_caches_for_current_thread
23+
if pool.dirties_query_cache
24+
ActiveRecord::Base.clear_query_caches_for_current_thread
25+
end
2426
super
2527
end
2628
end_code
@@ -29,13 +31,15 @@ def #{method_name}(...)
2931
end
3032

3133
class Store # :nodoc:
32-
attr_accessor :enabled
34+
attr_accessor :enabled, :dirties
3335
alias_method :enabled?, :enabled
36+
alias_method :dirties?, :dirties
3437

3538
def initialize(max_size)
3639
@map = {}
3740
@max_size = max_size
3841
@enabled = false
42+
@dirties = true
3943
end
4044

4145
def size
@@ -96,38 +100,42 @@ def lease_connection
96100
end
97101

98102
# Disable the query cache within the block.
99-
def disable_query_cache
103+
def disable_query_cache(dirties: true)
100104
cache = query_cache
101-
old, cache.enabled = cache.enabled, false
105+
old_enabled, cache.enabled, old_dirties, cache.dirties = cache.enabled, false, cache.dirties, dirties
102106
begin
103107
yield
104108
ensure
105-
cache.enabled = old
109+
cache.enabled, cache.dirties = old_enabled, old_dirties
106110
end
107111
end
108112

109113
def enable_query_cache
110114
cache = query_cache
111-
old, cache.enabled = cache.enabled, true
115+
old_enabled, cache.enabled, old_dirties, cache.dirties = cache.enabled, true, cache.dirties, true
112116
begin
113117
yield
114118
ensure
115-
cache.enabled = old
119+
cache.enabled, cache.dirties = old_enabled, old_dirties
116120
end
117121
end
118122

119123
def enable_query_cache!
120-
query_cache.enabled = true
124+
query_cache.enabled, query_cache.dirties = true, true
121125
end
122126

123127
def disable_query_cache!
124-
query_cache.enabled = false
128+
query_cache.enabled, query_cache.dirties = false, true
125129
end
126130

127131
def query_cache_enabled
128132
query_cache.enabled
129133
end
130134

135+
def dirties_query_cache
136+
query_cache.dirties
137+
end
138+
131139
def clear_query_cache
132140
if @pinned_connection
133141
# With transactional fixtures, and especially systems test
@@ -175,8 +183,11 @@ def enable_query_cache!
175183
end
176184

177185
# Disable the query cache within the block.
178-
def uncached(&)
179-
pool.disable_query_cache(&)
186+
#
187+
# Set <tt>dirties: false</tt> to prevent query caches on all connections from being cleared by write operations.
188+
# (By default, write operations dirty all connections' query caches in case they are replicas whose cache would now be outdated.)
189+
def uncached(dirties: true, &)
190+
pool.disable_query_cache(dirties: dirties, &)
180191
end
181192

182193
def disable_query_cache!

activerecord/lib/active_record/query_cache.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ def cache(&block)
2222

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

activerecord/test/cases/query_cache_test.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,57 @@ def test_clear_query_cache_is_called_on_all_connections
706706
end
707707
end
708708

709+
def test_query_cache_uncached_dirties
710+
mw = middleware { |env|
711+
Post.first
712+
assert_no_changes -> { ActiveRecord::Base.connection.query_cache.size } do
713+
Post.uncached(dirties: false) { Post.create!(title: "a new post", body: "and a body") }
714+
end
715+
716+
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
717+
Post.uncached(dirties: true) { Post.create!(title: "a new post", body: "and a body") }
718+
end
719+
}
720+
mw.call({})
721+
end
722+
723+
def test_query_cache_connection_uncached_dirties
724+
mw = middleware { |env|
725+
Post.first
726+
assert_no_changes -> { ActiveRecord::Base.connection.query_cache.size } do
727+
Post.connection.uncached(dirties: false) { Post.create!(title: "a new post", body: "and a body") }
728+
end
729+
730+
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
731+
Post.connection.uncached(dirties: true) { Post.create!(title: "a new post", body: "and a body") }
732+
end
733+
}
734+
mw.call({})
735+
end
736+
737+
def test_query_cache_uncached_dirties_disabled_with_nested_cache
738+
mw = middleware { |env|
739+
Post.first
740+
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
741+
Post.uncached(dirties: false) do
742+
Post.cache do
743+
Post.create!(title: "a new post", body: "and a body")
744+
end
745+
end
746+
end
747+
748+
Post.first
749+
assert_changes -> { ActiveRecord::Base.connection.query_cache.size }, from: 1, to: 0 do
750+
Post.connection.uncached(dirties: false) do
751+
Post.connection.cache do
752+
Post.create!(title: "a new post", body: "and a body")
753+
end
754+
end
755+
end
756+
}
757+
mw.call({})
758+
end
759+
709760
private
710761
def with_temporary_connection_pool(&block)
711762
pool_config = ActiveRecord::Base.lease_connection.pool.pool_config

0 commit comments

Comments
 (0)