Skip to content

Commit 85742ce

Browse files
committed
Refactor QueryCache to be owned by the pool
Ref: rails#50793 If we want to stop caching the checked out connections, then we must persist the cache in the pool, and assign it to the connection when it's checked out. The pool become responsible for managing the cache lifecycle. This also open the door to sharing the cache between multiple connections, which is valuable for read replicas, etc. This change only really make sense if we go through with no longer caching checked out connections. Otherwise it's just extra complexity.
1 parent cef567e commit 85742ce

File tree

4 files changed

+143
-77
lines changed

4 files changed

+143
-77
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def db_config
113113
# are now explicitly documented
114114
class ConnectionPool
115115
include MonitorMixin
116-
include QueryCache::ConnectionPoolConfiguration
116+
prepend QueryCache::ConnectionPoolConfiguration
117117
include ConnectionAdapters::AbstractPool
118118

119119
attr_accessor :automatic_reconnect, :checkout_timeout

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

Lines changed: 130 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ def included(base) # :nodoc:
1313
:truncate_tables, :rollback_to_savepoint, :rollback_db_transaction, :restart_db_transaction,
1414
:exec_insert_all
1515

16-
base.set_callback :checkout, :after, :configure_query_cache!
17-
base.set_callback :checkin, :after, :disable_query_cache!
16+
base.set_callback :checkin, :after, :unset_query_cache!
1817
end
1918

2019
def dirties_query_cache(base, *method_names)
@@ -29,60 +28,146 @@ def #{method_name}(...)
2928
end
3029
end
3130

32-
module ConnectionPoolConfiguration
33-
def initialize(*)
31+
class Store # :nodoc:
32+
attr_reader :enabled
33+
alias_method :enabled?, :enabled
34+
35+
def initialize(max_size)
36+
@map = {}
37+
@max_size = max_size
38+
@enabled = false
39+
end
40+
41+
def enabled=(enabled)
42+
clear if @enabled && !enabled
43+
@enabled = enabled
44+
end
45+
46+
def size
47+
@map.size
48+
end
49+
50+
def empty?
51+
@map.empty?
52+
end
53+
54+
def [](key)
55+
return unless @enabled
56+
57+
if entry = @map.delete(key)
58+
@map[key] = entry
59+
end
60+
end
61+
62+
def compute_if_absent(key)
63+
return yield unless @enabled
64+
65+
if entry = @map.delete(key)
66+
return @map[key] = entry
67+
end
68+
69+
if @max_size && @map.size >= @max_size
70+
@map.shift # evict the oldest entry
71+
end
72+
73+
@map[key] ||= yield
74+
end
75+
76+
def clear
77+
@map.clear
78+
self
79+
end
80+
end
81+
82+
module ConnectionPoolConfiguration # :nodoc:
83+
def initialize(...)
3484
super
35-
@query_cache_enabled = Concurrent::Map.new { false }
85+
@thread_query_caches = Concurrent::Map.new(initial_capacity: @size)
86+
@query_cache_max_size = \
87+
case query_cache = db_config&.query_cache
88+
when 0, false
89+
nil
90+
when Integer
91+
query_cache
92+
when nil
93+
DEFAULT_SIZE
94+
end
95+
end
96+
97+
def checkout(...)
98+
connection = super
99+
connection.query_cache ||= query_cache
100+
connection
101+
end
102+
103+
# Disable the query cache within the block.
104+
def disable_query_cache
105+
cache = query_cache
106+
old, cache.enabled = cache.enabled, false
107+
begin
108+
yield
109+
ensure
110+
cache.enabled = old
111+
end
112+
end
113+
114+
def enable_query_cache
115+
cache = query_cache
116+
old, cache.enabled = cache.enabled, true
117+
begin
118+
yield
119+
ensure
120+
cache.enabled = old
121+
end
36122
end
37123

38124
def enable_query_cache!
39-
@query_cache_enabled[connection_cache_key(ActiveSupport::IsolatedExecutionState.context)] = true
40-
connection.enable_query_cache! if active_connection?
125+
query_cache.enabled = true
41126
end
42127

43128
def disable_query_cache!
44-
@query_cache_enabled.delete connection_cache_key(ActiveSupport::IsolatedExecutionState.context)
45-
connection.disable_query_cache! if active_connection?
129+
query_cache.enabled = false
46130
end
47131

48132
def query_cache_enabled
49-
@query_cache_enabled[connection_cache_key(ActiveSupport::IsolatedExecutionState.context)]
133+
query_cache.enabled
50134
end
135+
136+
private
137+
def query_cache
138+
@thread_query_caches.compute_if_absent(connection_cache_key(ActiveSupport::IsolatedExecutionState.context)) do
139+
Store.new(@query_cache_max_size)
140+
end
141+
end
51142
end
52143

53-
attr_reader :query_cache, :query_cache_enabled
144+
attr_accessor :query_cache
54145

55146
def initialize(*)
56147
super
57-
@query_cache = {}
58-
@query_cache_enabled = false
59-
@query_cache_max_size = nil
148+
@query_cache = nil
149+
end
150+
151+
def query_cache_enabled
152+
@query_cache&.enabled?
60153
end
61154

62155
# Enable the query cache within the block.
63-
def cache
64-
old, @query_cache_enabled = @query_cache_enabled, true
65-
yield
66-
ensure
67-
@query_cache_enabled = old
68-
clear_query_cache unless @query_cache_enabled
156+
def cache(&)
157+
pool.enable_query_cache(&)
69158
end
70159

71160
def enable_query_cache!
72-
@query_cache_enabled = true
161+
pool.enable_query_cache!
73162
end
74163

75-
def disable_query_cache!
76-
@query_cache_enabled = false
77-
clear_query_cache
164+
# Disable the query cache within the block.
165+
def uncached(&)
166+
pool.disable_query_cache(&)
78167
end
79168

80-
# Disable the query cache within the block.
81-
def uncached
82-
old, @query_cache_enabled = @query_cache_enabled, false
83-
yield
84-
ensure
85-
@query_cache_enabled = old
169+
def disable_query_cache!
170+
pool.disable_query_cache!
86171
end
87172

88173
# Clears the query cache.
@@ -93,7 +178,7 @@ def uncached
93178
# undermining the randomness you were expecting.
94179
def clear_query_cache
95180
@lock.synchronize do
96-
@query_cache.clear
181+
@query_cache&.clear
97182
end
98183
end
99184

@@ -102,7 +187,7 @@ def select_all(arel, name = nil, binds = [], preparable: nil, async: false) # :n
102187

103188
# If arel is locked this is a SELECT ... FOR UPDATE or somesuch.
104189
# Such queries should not be cached.
105-
if @query_cache_enabled && !(arel.respond_to?(:locked) && arel.locked)
190+
if @query_cache&.enabled? && !(arel.respond_to?(:locked) && arel.locked)
106191
sql, binds, preparable = to_sql_and_binds(arel, binds, preparable)
107192

108193
if async
@@ -117,42 +202,37 @@ def select_all(arel, name = nil, binds = [], preparable: nil, async: false) # :n
117202
end
118203

119204
private
205+
def unset_query_cache!
206+
@query_cache = nil
207+
end
208+
120209
def lookup_sql_cache(sql, name, binds)
121210
key = binds.empty? ? sql : [sql, binds]
122-
hit = false
123-
result = nil
124211

212+
result = nil
125213
@lock.synchronize do
126-
if (result = @query_cache.delete(key))
127-
hit = true
128-
@query_cache[key] = result
129-
end
214+
result = @query_cache[key]
130215
end
131216

132-
if hit
217+
if result
133218
ActiveSupport::Notifications.instrument(
134219
"sql.active_record",
135220
cache_notification_info(sql, name, binds)
136221
)
137-
138-
result
139222
end
223+
224+
result
140225
end
141226

142227
def cache_sql(sql, name, binds)
143228
key = binds.empty? ? sql : [sql, binds]
144229
result = nil
145-
hit = false
230+
hit = true
146231

147232
@lock.synchronize do
148-
if (result = @query_cache.delete(key))
149-
hit = true
150-
@query_cache[key] = result
151-
else
152-
result = @query_cache[key] = yield
153-
if @query_cache_max_size && @query_cache.size > @query_cache_max_size
154-
@query_cache.shift
155-
end
233+
result = @query_cache.compute_if_absent(key) do
234+
hit = false
235+
yield
156236
end
157237
end
158238

@@ -178,23 +258,6 @@ def cache_notification_info(sql, name, binds)
178258
cached: true
179259
}
180260
end
181-
182-
def configure_query_cache!
183-
case query_cache = pool.db_config.query_cache
184-
when 0, false
185-
return
186-
when Integer
187-
@query_cache_max_size = query_cache
188-
when nil
189-
@query_cache_max_size = DEFAULT_SIZE
190-
else
191-
@query_cache_max_size = nil # no limit
192-
end
193-
194-
if pool.query_cache_enabled
195-
enable_query_cache!
196-
end
197-
end
198261
end
199262
end
200263
end

activerecord/lib/active_record/query_cache.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module ClassMethods
88
# If it's not, it will execute the given block.
99
def cache(&block)
1010
if connected? || !configurations.empty?
11-
connection.cache(&block)
11+
connection_pool.enable_query_cache(&block)
1212
else
1313
yield
1414
end
@@ -18,19 +18,19 @@ def cache(&block)
1818
# If it's not, it will execute the given block.
1919
def uncached(&block)
2020
if connected? || !configurations.empty?
21-
connection.uncached(&block)
21+
connection_pool.disable_query_cache(&block)
2222
else
2323
yield
2424
end
2525
end
2626
end
2727

2828
def self.run
29-
ActiveRecord::Base.connection_handler.each_connection_pool.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
29+
ActiveRecord::Base.connection_handler.each_connection_pool.reject(&:query_cache_enabled).each(&:enable_query_cache!)
3030
end
3131

3232
def self.complete(pools)
33-
pools.each { |pool| pool.disable_query_cache! }
33+
pools.each(&:disable_query_cache!)
3434

3535
ActiveRecord::Base.connection_handler.each_connection_pool do |pool|
3636
pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?

activerecord/test/cases/query_cache_test.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ def test_cache_gets_cleared_after_migration
808808
end
809809

810810
def test_find
811-
assert_called(Task.connection.query_cache, :clear) do
811+
assert_called(Task.connection.query_cache, :clear, times: 2) do
812812
assert_not Task.connection.query_cache_enabled
813813
Task.cache do
814814
assert Task.connection.query_cache_enabled
@@ -922,9 +922,12 @@ def test_cache_is_expired_by_habtm_delete
922922
end
923923

924924
def test_query_cache_lru_eviction
925+
store = ActiveRecord::ConnectionAdapters::QueryCache::Store.new(2)
926+
store.enabled = true
927+
925928
connection = Post.connection
926-
connection.pool.db_config.stub(:query_cache, 2) do
927-
connection.send(:configure_query_cache!)
929+
old_store, connection.query_cache = connection.query_cache, store
930+
begin
928931
Post.cache do
929932
assert_queries_count(2) do
930933
connection.select_all("SELECT 1")
@@ -945,9 +948,9 @@ def test_query_cache_lru_eviction
945948
connection.select_all("SELECT 2")
946949
end
947950
end
951+
ensure
952+
connection.query_cache = old_store
948953
end
949-
ensure
950-
connection.send(:configure_query_cache!)
951954
end
952955

953956
test "threads use the same connection" do

0 commit comments

Comments
 (0)