Skip to content

Commit 13c1dfe

Browse files
authored
Merge pull request rails#50938 from Shopify/refactor-query-cache-to-pool
Refactor QueryCache to be owned by the pool
2 parents cef567e + 94fc536 commit 13c1dfe

File tree

4 files changed

+165
-92
lines changed

4 files changed

+165
-92
lines changed

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

Lines changed: 16 additions & 16 deletions
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
@@ -176,13 +176,13 @@ def initialize(pool_config)
176176
# #connection can be called any number of times; the connection is
177177
# held in a cache keyed by a thread.
178178
def connection
179-
@thread_cached_conns[connection_cache_key(ActiveSupport::IsolatedExecutionState.context)] ||= checkout
179+
@thread_cached_conns[ActiveSupport::IsolatedExecutionState.context] ||= checkout
180180
end
181181

182182
def pin_connection!(lock_thread) # :nodoc:
183183
raise "There is already a pinned connection" if @pinned_connection
184184

185-
@pinned_connection = (@thread_cached_conns[connection_cache_key(ActiveSupport::IsolatedExecutionState.context)] || checkout)
185+
@pinned_connection = (@thread_cached_conns[ActiveSupport::IsolatedExecutionState.context] || checkout)
186186
# Any leased connection must be in @connections otherwise
187187
# some methods like #connected? won't behave correctly
188188
unless @connections.include?(@pinned_connection)
@@ -226,7 +226,7 @@ def connection_class # :nodoc:
226226
# #connection or #with_connection methods. Connections obtained through
227227
# #checkout will not be detected by #active_connection?
228228
def active_connection?
229-
@thread_cached_conns[connection_cache_key(ActiveSupport::IsolatedExecutionState.context)]
229+
@thread_cached_conns[ActiveSupport::IsolatedExecutionState.context]
230230
end
231231

232232
# Signal that the thread is finished with the current connection.
@@ -237,7 +237,7 @@ def active_connection?
237237
# #connection or #with_connection methods, connections obtained through
238238
# #checkout will not be automatically released.
239239
def release_connection(owner_thread = ActiveSupport::IsolatedExecutionState.context)
240-
if conn = @thread_cached_conns.delete(connection_cache_key(owner_thread))
240+
if conn = @thread_cached_conns.delete(owner_thread)
241241
checkin conn
242242
end
243243
end
@@ -252,7 +252,7 @@ def release_connection(owner_thread = ActiveSupport::IsolatedExecutionState.cont
252252
# connection will be properly returned to the pool by the code that checked
253253
# it out.
254254
def with_connection
255-
unless conn = @thread_cached_conns[connection_cache_key(ActiveSupport::IsolatedExecutionState.context)]
255+
unless conn = @thread_cached_conns[ActiveSupport::IsolatedExecutionState.context]
256256
conn = connection
257257
fresh_connection = true
258258
end
@@ -471,6 +471,8 @@ def reap
471471
remove conn
472472
end
473473
end
474+
475+
prune_thread_cache
474476
end
475477

476478
# Disconnect all connections that have been idle for at least
@@ -558,15 +560,6 @@ def bulk_make_new_connections(num_new_conns_needed)
558560
end
559561
end
560562

561-
#--
562-
# From the discussion on GitHub:
563-
# https://github.com/rails/rails/pull/14938#commitcomment-6601951
564-
# This hook-in method allows for easier monkey-patching fixes needed by
565-
# JRuby users that use Fibers.
566-
def connection_cache_key(thread)
567-
thread
568-
end
569-
570563
# Take control of all existing connections so a "group" action such as
571564
# reload/disconnect can be performed safely. It is no longer enough to
572565
# wrap it in +synchronize+ because some pool's actions are allowed
@@ -710,10 +703,17 @@ def acquire_connection(checkout_timeout)
710703
#--
711704
# if owner_thread param is omitted, this must be called in synchronize block
712705
def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
713-
@thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn)
706+
@thread_cached_conns.delete_pair(owner_thread, conn)
714707
end
715708
alias_method :release, :remove_connection_from_thread_cache
716709

710+
def prune_thread_cache
711+
dead_threads = @thread_cached_conns.keys.reject(&:alive?)
712+
dead_threads.each do |dead_thread|
713+
@thread_cached_conns.delete(dead_thread)
714+
end
715+
end
716+
717717
def new_connection
718718
connection = db_config.new_connection
719719
connection.pool = self

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

Lines changed: 137 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,153 @@ 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 prune_thread_cache
138+
dead_threads = @thread_query_caches.keys.reject(&:alive?)
139+
dead_threads.each do |dead_thread|
140+
@thread_query_caches.delete(dead_thread)
141+
end
142+
end
143+
144+
def query_cache
145+
@thread_query_caches.compute_if_absent(ActiveSupport::IsolatedExecutionState.context) do
146+
Store.new(@query_cache_max_size)
147+
end
148+
end
51149
end
52150

53-
attr_reader :query_cache, :query_cache_enabled
151+
attr_accessor :query_cache
54152

55153
def initialize(*)
56154
super
57-
@query_cache = {}
58-
@query_cache_enabled = false
59-
@query_cache_max_size = nil
155+
@query_cache = nil
156+
end
157+
158+
def query_cache_enabled
159+
@query_cache&.enabled?
60160
end
61161

62162
# 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
163+
def cache(&)
164+
pool.enable_query_cache(&)
69165
end
70166

71167
def enable_query_cache!
72-
@query_cache_enabled = true
168+
pool.enable_query_cache!
73169
end
74170

75-
def disable_query_cache!
76-
@query_cache_enabled = false
77-
clear_query_cache
171+
# Disable the query cache within the block.
172+
def uncached(&)
173+
pool.disable_query_cache(&)
78174
end
79175

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
176+
def disable_query_cache!
177+
pool.disable_query_cache!
86178
end
87179

88180
# Clears the query cache.
@@ -93,7 +185,7 @@ def uncached
93185
# undermining the randomness you were expecting.
94186
def clear_query_cache
95187
@lock.synchronize do
96-
@query_cache.clear
188+
@query_cache&.clear
97189
end
98190
end
99191

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

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

108200
if async
@@ -117,42 +209,37 @@ def select_all(arel, name = nil, binds = [], preparable: nil, async: false) # :n
117209
end
118210

119211
private
212+
def unset_query_cache!
213+
@query_cache = nil
214+
end
215+
120216
def lookup_sql_cache(sql, name, binds)
121217
key = binds.empty? ? sql : [sql, binds]
122-
hit = false
123-
result = nil
124218

219+
result = nil
125220
@lock.synchronize do
126-
if (result = @query_cache.delete(key))
127-
hit = true
128-
@query_cache[key] = result
129-
end
221+
result = @query_cache[key]
130222
end
131223

132-
if hit
224+
if result
133225
ActiveSupport::Notifications.instrument(
134226
"sql.active_record",
135227
cache_notification_info(sql, name, binds)
136228
)
137-
138-
result
139229
end
230+
231+
result
140232
end
141233

142234
def cache_sql(sql, name, binds)
143235
key = binds.empty? ? sql : [sql, binds]
144236
result = nil
145-
hit = false
237+
hit = true
146238

147239
@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
240+
result = @query_cache.compute_if_absent(key) do
241+
hit = false
242+
yield
156243
end
157244
end
158245

@@ -178,23 +265,6 @@ def cache_notification_info(sql, name, binds)
178265
cached: true
179266
}
180267
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
198268
end
199269
end
200270
end

0 commit comments

Comments
 (0)