Skip to content

Commit 4db9f51

Browse files
authored
Merge pull request rails#51349 from Shopify/connection-optional-deprecation
Add `config.active_record.permanent_connection_checkout` setting
2 parents 6b6a60c + fdde675 commit 4db9f51

File tree

8 files changed

+206
-21
lines changed

8 files changed

+206
-21
lines changed

activerecord/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
* Add `config.active_record.permanent_connection_checkout` setting.
2+
3+
Controls whether `ActiveRecord::Base.connection` raises an error, emits a deprecation warning, or neither.
4+
5+
`ActiveRecord::Base.connection` checkouts a database connection from the pool and keep it leased until the end of
6+
the request or job. This behavior can be undesirable in environments that use many more threads or fibers than there
7+
is available connections.
8+
9+
This configuration can be used to track down and eliminate code that calls `ActiveRecord::Base.connection` and
10+
migrate it to use `ActiveRecord::Base.with_connection` instead.
11+
12+
The default behavior remains unchanged, and there is currently no plans to change the default.
13+
14+
*Jean Boussier*
15+
116
* Add dirties option to uncached
217

318
This adds a `dirties` option to `ActiveRecord::Base.uncached` and

activerecord/lib/active_record.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,17 @@ def self.global_executor_concurrency # :nodoc:
300300
@global_executor_concurrency ||= nil
301301
end
302302

303+
@permanent_connection_checkout = true
304+
singleton_class.attr_reader :permanent_connection_checkout
305+
306+
# Defines whether +ActiveRecord::Base.connection+ is allowed, deprecated or entirely disallowed
307+
def self.permanent_connection_checkout=(value)
308+
unless [true, :deprecated, :disallowed].include?(value)
309+
raise ArgumentError, "permanent_connection_checkout must be one of: `true`, `:deprecated` or `:disallowed`"
310+
end
311+
@permanent_connection_checkout = value
312+
end
313+
303314
singleton_class.attr_accessor :index_nested_attribute_errors
304315
self.index_nested_attribute_errors = false
305316

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,20 +123,20 @@ class Lease # :nodoc:
123123

124124
def initialize
125125
@connection = nil
126-
@sticky = false
126+
@sticky = nil
127127
end
128128

129129
def release
130130
conn = @connection
131131
@connection = nil
132-
@sticky = false
132+
@sticky = nil
133133
conn
134134
end
135135

136136
def clear(connection)
137137
if @connection == connection
138138
@connection = nil
139-
@sticky = false
139+
@sticky = nil
140140
true
141141
else
142142
false
@@ -290,14 +290,18 @@ def internal_metadata # :nodoc:
290290
# Retrieve the connection associated with the current thread, or call
291291
# #checkout to obtain one if necessary.
292292
#
293-
# #connection can be called any number of times; the connection is
293+
# #lease_connection can be called any number of times; the connection is
294294
# held in a cache keyed by a thread.
295295
def lease_connection
296296
lease = connection_lease
297297
lease.sticky = true
298298
lease.connection ||= checkout
299299
end
300300

301+
def permanent_lease? # :nodoc:
302+
connection_lease.sticky.nil?
303+
end
304+
301305
def connection
302306
ActiveRecord.deprecator.warn(<<~MSG)
303307
ActiveRecord::ConnectionAdapters::ConnectionPool#connection is deprecated
@@ -348,18 +352,19 @@ def connection_class # :nodoc:
348352
# Returns true if there is an open connection being used for the current thread.
349353
#
350354
# This method only works for connections that have been obtained through
351-
# #connection or #with_connection methods. Connections obtained through
355+
# #lease_connection or #with_connection methods. Connections obtained through
352356
# #checkout will not be detected by #active_connection?
353357
def active_connection?
354358
connection_lease.connection
355359
end
360+
alias_method :active_connection, :active_connection? # :nodoc:
356361

357362
# Signal that the thread is finished with the current connection.
358363
# #release_connection releases the connection-thread association
359364
# and returns the connection to the pool.
360365
#
361366
# This method only works for connections that have been obtained through
362-
# #connection or #with_connection methods, connections obtained through
367+
# #lease_connection or #with_connection methods, connections obtained through
363368
# #checkout will not be automatically released.
364369
def release_connection(existing_lease = nil)
365370
if conn = connection_lease.release
@@ -373,19 +378,27 @@ def release_connection(existing_lease = nil)
373378
# is already checked out by the current thread, a connection will be checked
374379
# out from the pool, yielded to the block, and then returned to the pool when
375380
# the block is finished. If a connection has already been checked out on the
376-
# current thread, such as via #connection or #with_connection, that existing
381+
# current thread, such as via #lease_connection or #with_connection, that existing
377382
# connection will be the one yielded and it will not be returned to the pool
378383
# automatically at the end of the block; it is expected that such an existing
379384
# connection will be properly returned to the pool by the code that checked
380385
# it out.
381-
def with_connection
386+
def with_connection(prevent_permanent_checkout: false)
382387
lease = connection_lease
388+
sticky_was = lease.sticky
389+
lease.sticky = false if prevent_permanent_checkout
390+
383391
if lease.connection
384-
yield lease.connection
392+
begin
393+
yield lease.connection
394+
ensure
395+
lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
396+
end
385397
else
386398
begin
387399
yield lease.connection = checkout
388400
ensure
401+
lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
389402
release_connection(lease) unless lease.sticky
390403
end
391404
end

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def initialize(...)
9393
end
9494
end
9595

96-
def lease_connection
96+
def lease_connection(**)
9797
connection = super
9898
connection.query_cache ||= query_cache
9999
connection

activerecord/lib/active_record/connection_handling.rb

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,18 +256,44 @@ def lease_connection
256256
connection_pool.lease_connection
257257
end
258258

259-
alias_method :connection, :lease_connection
259+
# Soft deprecated. Use +#with_connection+ or +#lease_connection+ instead.
260+
def connection
261+
pool = connection_pool
262+
if pool.permanent_lease?
263+
case ActiveRecord.permanent_connection_checkout
264+
when :deprecated
265+
ActiveRecord.deprecator.warn <<~MESSAGE
266+
Called deprecated `ActionRecord::Base.connection`method.
267+
268+
Either use `with_connection` or `lease_connection`.
269+
MESSAGE
270+
when :disallowed
271+
raise ActiveRecordError, <<~MESSAGE
272+
Called deprecated `ActionRecord::Base.connection`method.
273+
274+
Either use `with_connection` or `lease_connection`.
275+
MESSAGE
276+
end
277+
pool.lease_connection
278+
else
279+
pool.active_connection
280+
end
281+
end
260282

261283
# Return the currently leased connection into the pool
262284
def release_connection
263-
connection.release_connection
285+
connection_pool.release_connection
264286
end
265287

266288
# Checkouts a connection from the pool, yield it and then check it back in.
267-
# If a connection was already leased via #connection or a parent call to
289+
# If a connection was already leased via #lease_connection or a parent call to
268290
# #with_connection, that same connection is yieled.
269-
def with_connection(&block)
270-
connection_pool.with_connection(&block)
291+
# If #lease_connection is called inside the block, the connection won't be checked
292+
# back in.
293+
# If #connection is called inside the block, the connection won't be checked back in
294+
# unless the +prevent_permanent_checkout+ argument is set to +true+.
295+
def with_connection(prevent_permanent_checkout: false, &block)
296+
connection_pool.with_connection(prevent_permanent_checkout: prevent_permanent_checkout, &block)
271297
end
272298

273299
attr_writer :connection_specification_name

activerecord/test/cases/connection_handling_test.rb

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@
44

55
module ActiveRecord
66
class ConnectionHandlingTest < ActiveRecord::TestCase
7+
setup do
8+
@_permanent_connection_checkout_was = ActiveRecord.permanent_connection_checkout
9+
end
10+
11+
teardown do
12+
ActiveRecord.permanent_connection_checkout = @_permanent_connection_checkout_was
13+
end
14+
715
unless in_memory_db?
816
test "#with_connection lease the connection for the duration of the block" do
9-
ActiveRecord::Base.connection_pool.release_connection
17+
ActiveRecord::Base.release_connection
1018
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
1119

1220
ActiveRecord::Base.with_connection do |connection|
@@ -16,8 +24,8 @@ class ConnectionHandlingTest < ActiveRecord::TestCase
1624
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
1725
end
1826

19-
test "#connection makes the lease permanent even inside #with_connection" do
20-
ActiveRecord::Base.connection_pool.release_connection
27+
test "#lease_connection makes the lease permanent even inside #with_connection" do
28+
ActiveRecord::Base.release_connection
2129
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
2230

2331
conn = nil
@@ -33,6 +41,16 @@ class ConnectionHandlingTest < ActiveRecord::TestCase
3341
assert_same conn, ActiveRecord::Base.lease_connection
3442
end
3543

44+
test "#lease_connection makes the lease permanent even inside #with_connection(prevent_permanent_checkout: true)" do
45+
ActiveRecord::Base.release_connection
46+
47+
ActiveRecord::Base.with_connection(prevent_permanent_checkout: true) do |connection|
48+
assert_same connection, ActiveRecord::Base.lease_connection
49+
end
50+
51+
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
52+
end
53+
3654
test "#with_connection use the already leased connection if available" do
3755
leased_connection = ActiveRecord::Base.lease_connection
3856
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
@@ -63,6 +81,89 @@ class ConnectionHandlingTest < ActiveRecord::TestCase
6381
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
6482
assert_same ActiveRecord::Base.lease_connection, leased_connection
6583
end
84+
85+
test "#connection is a soft-deprecated alias to #lease_connection" do
86+
ActiveRecord.permanent_connection_checkout = true
87+
88+
ActiveRecord::Base.release_connection
89+
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
90+
91+
conn = nil
92+
ActiveRecord::Base.with_connection do |connection|
93+
conn = connection
94+
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
95+
2.times do
96+
assert_same connection, ActiveRecord::Base.connection
97+
end
98+
end
99+
100+
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
101+
assert_same conn, ActiveRecord::Base.connection
102+
103+
ActiveRecord::Base.release_connection
104+
end
105+
106+
test "#connection emits a deprecation warning if ActiveRecord.permanent_connection_checkout == :deprecated" do
107+
ActiveRecord.permanent_connection_checkout = :deprecated
108+
109+
ActiveRecord::Base.release_connection
110+
111+
assert_deprecated(ActiveRecord.deprecator) do
112+
ActiveRecord::Base.connection
113+
end
114+
115+
assert_not_deprecated(ActiveRecord.deprecator) do
116+
ActiveRecord::Base.connection
117+
end
118+
119+
ActiveRecord::Base.release_connection
120+
121+
assert_deprecated(ActiveRecord.deprecator) do
122+
ActiveRecord::Base.connection
123+
end
124+
125+
ActiveRecord::Base.release_connection
126+
127+
ActiveRecord::Base.with_connection do
128+
assert_deprecated(ActiveRecord.deprecator) do
129+
ActiveRecord::Base.connection
130+
end
131+
end
132+
end
133+
134+
test "#connection raises an error if ActiveRecord.permanent_connection_checkout == :disallowed" do
135+
ActiveRecord.permanent_connection_checkout = :disallowed
136+
137+
ActiveRecord::Base.release_connection
138+
139+
assert_raises(ActiveRecordError) do
140+
ActiveRecord::Base.connection
141+
end
142+
143+
ActiveRecord::Base.with_connection do
144+
assert_raises(ActiveRecordError) do
145+
ActiveRecord::Base.connection
146+
end
147+
end
148+
149+
ActiveRecord::Base.lease_connection
150+
151+
assert_nothing_raised do
152+
ActiveRecord::Base.connection
153+
end
154+
end
155+
156+
test "#connection doesn't make the lease permanent if inside #with_connection(prevent_permanent_checkout: true)" do
157+
ActiveRecord.permanent_connection_checkout = :disallowed
158+
159+
ActiveRecord::Base.release_connection
160+
161+
ActiveRecord::Base.with_connection(prevent_permanent_checkout: true) do |connection|
162+
assert_same connection, ActiveRecord::Base.connection
163+
end
164+
165+
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
166+
end
66167
end
67168
end
68169
end

activerecord/test/cases/helper.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
# Show backtraces for deprecated behavior for quicker cleanup.
2323
ActiveRecord.deprecator.debug = true
2424

25-
# ActiveRecord::Base.connection is only soft deprecated but we remove it
26-
# in the test suite to ensure we're not using it internally.
27-
ActiveRecord::ConnectionHandling.remove_method(:connection)
25+
# ActiveRecord::Base.connection is only soft deprecated but we ban it from the test suite
26+
# to ensure it's not used internally.
27+
ActiveRecord.permanent_connection_checkout = :disallowed
2828

2929
# Disable available locale checks to avoid warnings running the test suite.
3030
I18n.enforce_available_locales = false

guides/source/configuring.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,25 @@ record.token # => "fwZcXX6SkJBJRogzMdciS7wf"
15261526
| (original) | `:create` |
15271527
| 7.1 | `:initialize` |
15281528

1529+
1530+
#### `config.active_record.permanent_connection_checkout`
1531+
1532+
Controls whether `ActiveRecord::Base.connection` raises an error, emits a deprecation warning, or neither.
1533+
1534+
`ActiveRecord::Base.connection` checkouts a database connection from the pool and keep it leased until the end of
1535+
the request or job. This behavior can be undesirable in environments that use many more threads or fibers than there
1536+
is available connections.
1537+
1538+
This configuration can be used to track down and eliminate code that calls `ActiveRecord::Base.connection` and
1539+
migrate it to use `ActiveRecord::Base.with_connection` instead.
1540+
1541+
The value can be set to `:disallowed`, `:deprecated` or `true` to respectively raise an error, emit a deprecation
1542+
warning, or neither.
1543+
1544+
| Starting with version | The default value is |
1545+
| --------------------- | -------------------- |
1546+
| (original) | `true` |
1547+
15291548
#### `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` and `ActiveRecord::ConnectionAdapters::TrilogyAdapter.emulate_booleans`
15301549

15311550
Controls whether the Active Record MySQL adapter will consider all `tinyint(1)` columns as booleans. Defaults to `true`.

0 commit comments

Comments
 (0)