Skip to content

Commit e7d2d11

Browse files
committed
[ActiveRecord] Introduce with_default_isolation_level
1 parent 2318163 commit e7d2d11

File tree

4 files changed

+65
-0
lines changed

4 files changed

+65
-0
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,16 @@ def new_connection # :nodoc:
706706
raise ex.set_pool(self)
707707
end
708708

709+
def default_isolation_level
710+
isolation_level_key = "activerecord_default_isolation_level_#{db_config.name}"
711+
ActiveSupport::IsolatedExecutionState[isolation_level_key]
712+
end
713+
714+
def default_isolation_level=(isolation_level)
715+
isolation_level_key = "activerecord_default_isolation_level_#{db_config.name}"
716+
ActiveSupport::IsolatedExecutionState[isolation_level_key] = isolation_level
717+
end
718+
709719
private
710720
def connection_lease
711721
@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)