Skip to content

Commit b7e8b60

Browse files
authored
Merge pull request rails#54836 from kirs/with_default_isolation_level
[ActiveRecord] Introduce with_default_isolation_level
2 parents b97a762 + f5e0264 commit b7e8b60

File tree

4 files changed

+72
-2
lines changed

4 files changed

+72
-2
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ module AbstractPool # :nodoc:
1414
class NullPool # :nodoc:
1515
include ConnectionAdapters::AbstractPool
1616

17-
class NullConfig # :nodoc:
17+
class NullConfig
1818
def method_missing(...)
1919
nil
2020
end
2121
end
22-
NULL_CONFIG = NullConfig.new # :nodoc:
22+
NULL_CONFIG = NullConfig.new
2323

2424
def initialize
2525
super()
@@ -48,6 +48,11 @@ def db_config
4848
def dirties_query_cache
4949
true
5050
end
51+
52+
def default_isolation_level; end
53+
def default_isolation_level=(isolation_level)
54+
raise NotImplementedError, "This method should never be called"
55+
end
5156
end
5257

5358
# = Active Record Connection Pool
@@ -706,6 +711,16 @@ def new_connection # :nodoc:
706711
raise ex.set_pool(self)
707712
end
708713

714+
def default_isolation_level
715+
isolation_level_key = "activerecord_default_isolation_level_#{db_config.name}"
716+
ActiveSupport::IsolatedExecutionState[isolation_level_key]
717+
end
718+
719+
def default_isolation_level=(isolation_level)
720+
isolation_level_key = "activerecord_default_isolation_level_#{db_config.name}"
721+
ActiveSupport::IsolatedExecutionState[isolation_level_key] = isolation_level
722+
end
723+
709724
private
710725
def connection_lease
711726
@leases[ActiveSupport::IsolatedExecutionState.context]

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ def rollback_transaction(transaction = nil)
620620
end
621621

622622
def within_new_transaction(isolation: nil, joinable: true)
623+
isolation ||= @connection.pool.default_isolation_level
623624
@connection.lock.synchronize do
624625
transaction = begin_transaction(isolation: isolation, joinable: joinable)
625626
begin

activerecord/lib/active_record/transactions.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,25 @@ def transaction(**options, &block)
234234
end
235235
end
236236

237+
# Makes all transactions initiated within the block use the isolation level
238+
# that you set as the default. Useful for gradually migrating apps onto new isolation level.
239+
def with_default_isolation_level(isolation_level, &block)
240+
if current_transaction.open?
241+
raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
242+
end
243+
244+
old_level = connection_pool.default_isolation_level
245+
connection_pool.default_isolation_level = isolation_level
246+
yield
247+
ensure
248+
connection_pool.default_isolation_level = old_level
249+
end
250+
251+
# Returns the default isolation level for the connection pool, set earlier by #with_default_isolation_level.
252+
def default_isolation_level
253+
connection_pool.default_isolation_level
254+
end
255+
237256
# Returns a representation of the current transaction state,
238257
# which can be a top level transaction, a savepoint, or the absence of a transaction.
239258
#

activerecord/test/cases/transaction_isolation_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ class Tag2 < ActiveRecord::Base
6262
assert_equal 1, Tag.count
6363
end
6464

65+
test "default_isolation_level" do
66+
assert_nil Tag.default_isolation_level
67+
68+
events = []
69+
ActiveSupport::Notifications.subscribed(
70+
-> (event) { events << event.payload[:sql] },
71+
"sql.active_record",
72+
) do
73+
Tag.with_default_isolation_level(:read_committed) do
74+
assert_equal :read_committed, Tag.default_isolation_level
75+
Tag.transaction do
76+
Tag.create!(name: "jon")
77+
end
78+
end
79+
end
80+
assert_begin_isolation_level_event(events)
81+
end
82+
83+
test "default_isolation_level cannot be set within open transaction" do
84+
assert_raises(ActiveRecord::TransactionIsolationError) do
85+
Tag.transaction do
86+
Tag.with_default_isolation_level(:read_committed) { }
87+
end
88+
end
89+
end
90+
6591
# We are testing that a nonrepeatable read does not happen
6692
if ActiveRecord::Base.lease_connection.transaction_isolation_levels.include?(:repeatable_read)
6793
test "repeatable read" do
@@ -106,5 +132,14 @@ class Tag2 < ActiveRecord::Base
106132
end
107133
end
108134
end
135+
136+
private
137+
def assert_begin_isolation_level_event(events)
138+
if current_adapter?(:PostgreSQLAdapter)
139+
assert_equal 1, events.select { _1.match(/BEGIN ISOLATION LEVEL READ COMMITTED/) }.size
140+
else
141+
assert_equal 1, events.select { _1.match(/SET TRANSACTION ISOLATION LEVEL READ COMMITTED/) }.size
142+
end
143+
end
109144
end
110145
end

0 commit comments

Comments
 (0)