Skip to content

Commit dd8fd52

Browse files
committed
Add config.active_record.permanent_connection_checkout setting
Controls whether `ActiveRecord::Base.connection` raises an error, emits a deprecation warning, or neither. `ActiveRecord::Base.connection` checkouts a database connection from the pool and keep it leased until the end of the request or job. This behavior can be undesirable in environments that use many more threads or fibers than there is available connections. This configuration can be used to track down and eliminate code that calls `ActiveRecord::Base.connection` and migrate it to use `ActiveRecord::Base.with_connection` instead. The default behavior remains unchanged, and there is currently no plans to change the default.
1 parent 30e3738 commit dd8fd52

File tree

8 files changed

+162
-14
lines changed

8 files changed

+162
-14
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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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 sticky_lease? # :nodoc:
302+
connection_lease.sticky
303+
end
304+
301305
def connection
302306
ActiveRecord.deprecator.warn(<<~MSG)
303307
ActiveRecord::ConnectionAdapters::ConnectionPool#connection is deprecated
@@ -348,7 +352,7 @@ 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
@@ -359,7 +363,7 @@ def active_connection?
359363
# and returns the connection to the pool.
360364
#
361365
# This method only works for connections that have been obtained through
362-
# #connection or #with_connection methods, connections obtained through
366+
# #lease_connection or #with_connection methods, connections obtained through
363367
# #checkout will not be automatically released.
364368
def release_connection(existing_lease = nil)
365369
if conn = connection_lease.release
@@ -373,7 +377,7 @@ def release_connection(existing_lease = nil)
373377
# is already checked out by the current thread, a connection will be checked
374378
# out from the pool, yielded to the block, and then returned to the pool when
375379
# 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
380+
# current thread, such as via #lease_connection or #with_connection, that existing
377381
# connection will be the one yielded and it will not be returned to the pool
378382
# automatically at the end of the block; it is expected that such an existing
379383
# connection will be properly returned to the pool by the code that checked

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: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,15 +256,35 @@ 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+
unless pool.sticky_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+
end
278+
pool.lease_connection
279+
end
260280

261281
# Return the currently leased connection into the pool
262282
def release_connection
263-
connection.release_connection
283+
connection_pool.release_connection
264284
end
265285

266286
# 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
287+
# If a connection was already leased via #lease_connection or a parent call to
268288
# #with_connection, that same connection is yieled.
269289
def with_connection(&block)
270290
connection_pool.with_connection(&block)

activerecord/test/cases/connection_handling_test.rb

Lines changed: 82 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
@@ -63,6 +71,77 @@ class ConnectionHandlingTest < ActiveRecord::TestCase
6371
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
6472
assert_same ActiveRecord::Base.lease_connection, leased_connection
6573
end
74+
75+
test "#connection is a soft-deprecated alias to #lease_connection" do
76+
ActiveRecord.permanent_connection_checkout = true
77+
78+
ActiveRecord::Base.release_connection
79+
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
80+
81+
conn = nil
82+
ActiveRecord::Base.with_connection do |connection|
83+
conn = connection
84+
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
85+
2.times do
86+
assert_same connection, ActiveRecord::Base.connection
87+
end
88+
end
89+
90+
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
91+
assert_same conn, ActiveRecord::Base.connection
92+
93+
ActiveRecord::Base.release_connection
94+
end
95+
96+
test "#connection emits a deprecation warning if ActiveRecord.permanent_connection_checkout == :deprecated" do
97+
ActiveRecord.permanent_connection_checkout = :deprecated
98+
99+
ActiveRecord::Base.release_connection
100+
101+
assert_deprecated(ActiveRecord.deprecator) do
102+
ActiveRecord::Base.connection
103+
end
104+
105+
assert_not_deprecated(ActiveRecord.deprecator) do
106+
ActiveRecord::Base.connection
107+
end
108+
109+
ActiveRecord::Base.release_connection
110+
111+
assert_deprecated(ActiveRecord.deprecator) do
112+
ActiveRecord::Base.connection
113+
end
114+
115+
ActiveRecord::Base.release_connection
116+
117+
ActiveRecord::Base.with_connection do
118+
assert_deprecated(ActiveRecord.deprecator) do
119+
ActiveRecord::Base.connection
120+
end
121+
end
122+
end
123+
124+
test "#connection raises an error if ActiveRecord.permanent_connection_checkout == :disallowed" do
125+
ActiveRecord.permanent_connection_checkout = :disallowed
126+
127+
ActiveRecord::Base.release_connection
128+
129+
assert_raises(ActiveRecordError) do
130+
ActiveRecord::Base.connection
131+
end
132+
133+
ActiveRecord::Base.with_connection do
134+
assert_raises(ActiveRecordError) do
135+
ActiveRecord::Base.connection
136+
end
137+
end
138+
139+
ActiveRecord::Base.lease_connection
140+
141+
assert_nothing_raised do
142+
ActiveRecord::Base.connection
143+
end
144+
end
66145
end
67146
end
68147
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)