Skip to content

Commit 4908dc3

Browse files
authored
Merge pull request rails#52529 from Shopify/mysql-begin-isolation-retry
Preserve isolation level when retrying MySQL transactions
2 parents 429f400 + 9414ed8 commit 4908dc3

File tree

7 files changed

+74
-19
lines changed

7 files changed

+74
-19
lines changed

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,16 @@ def select_rows(arel, name = nil, binds = [], async: false)
102102
select_all(arel, name, binds, async: async).then(&:rows)
103103
end
104104

105-
def query_value(sql, name = nil) # :nodoc:
106-
single_value_from_rows(query(sql, name))
105+
def query_value(...) # :nodoc:
106+
single_value_from_rows(query(...))
107107
end
108108

109-
def query_values(sql, name = nil) # :nodoc:
110-
query(sql, name).map(&:first)
109+
def query_values(...) # :nodoc:
110+
query(...).map(&:first)
111111
end
112112

113-
def query(sql, name = nil) # :nodoc:
114-
internal_exec_query(sql, name).rows
113+
def query(...) # :nodoc:
114+
internal_exec_query(...).rows
115115
end
116116

117117
# Determines whether the SQL statement is a write query.
@@ -591,9 +591,9 @@ def internal_execute(sql, name = "SQL", binds = [], prepare: false, async: false
591591
raw_execute(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions, &block)
592592
end
593593

594-
def execute_batch(statements, name = nil)
594+
def execute_batch(statements, name = nil, **kwargs)
595595
statements.each do |statement|
596-
raw_execute(statement, name)
596+
raw_execute(statement, name, **kwargs)
597597
end
598598
end
599599

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,12 @@ def begin_db_transaction # :nodoc:
227227
def begin_isolated_db_transaction(isolation) # :nodoc:
228228
# From MySQL manual: The [SET TRANSACTION] statement applies only to the next single transaction performed within the session.
229229
# So we don't need to implement #reset_isolation_level
230-
internal_execute("SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "TRANSACTION", allow_retry: true, materialize_transactions: false)
231-
begin_db_transaction
230+
execute_batch(
231+
["SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "BEGIN"],
232+
"TRANSACTION",
233+
allow_retry: true,
234+
materialize_transactions: false,
235+
)
232236
end
233237

234238
def commit_db_transaction # :nodoc:
@@ -565,7 +569,7 @@ def table_options(table_name) # :nodoc:
565569

566570
# SHOW VARIABLES LIKE 'name'
567571
def show_variable(name)
568-
query_value("SELECT @@#{name}", "SCHEMA")
572+
query_value("SELECT @@#{name}", "SCHEMA", materialize_transactions: false, allow_retry: true)
569573
rescue ActiveRecord::StatementInvalid
570574
nil
571575
end

activerecord/lib/active_record/connection_adapters/mysql2/database_statements.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ def select_all(*, **) # :nodoc:
1414
end
1515

1616
private
17-
def execute_batch(statements, name = nil)
17+
def execute_batch(statements, name = nil, **kwargs)
1818
combine_multi_statements(statements).each do |statement|
19-
raw_execute(statement, name, batch: true)
19+
raw_execute(statement, name, batch: true, **kwargs)
2020
end
2121
end
2222

activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ def affected_rows(result)
192192
affected_rows
193193
end
194194

195-
def execute_batch(statements, name = nil)
196-
raw_execute(combine_multi_statements(statements), name, batch: true)
195+
def execute_batch(statements, name = nil, **kwargs)
196+
raw_execute(combine_multi_statements(statements), name, batch: true, **kwargs)
197197
end
198198

199199
def build_truncate_statements(table_names)

activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ def affected_rows(result)
123123
@last_affected_rows
124124
end
125125

126-
def execute_batch(statements, name = nil)
126+
def execute_batch(statements, name = nil, **kwargs)
127127
sql = combine_multi_statements(statements)
128-
raw_execute(sql, name, batch: true)
128+
raw_execute(sql, name, batch: true, **kwargs)
129129
end
130130

131131
def build_truncate_statement(table_name)

activerecord/lib/active_record/connection_adapters/trilogy/database_statements.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ def last_inserted_id(result)
5858
end
5959
end
6060

61-
def execute_batch(statements, name = nil)
61+
def execute_batch(statements, name = nil, **kwargs)
6262
combine_multi_statements(statements).each do |statement|
63-
raw_execute(statement, name, batch: true)
63+
raw_execute(statement, name, batch: true, **kwargs)
6464
end
6565
end
6666
end

activerecord/test/cases/adapters/abstract_mysql_adapter/transaction_test.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,56 @@ class Sample < ActiveRecord::Base
149149
end
150150
assert_kind_of ActiveRecord::QueryAborted, error
151151
end
152+
153+
test "reconnect preserves isolation level" do
154+
pool = Sample.connection_pool
155+
@connection = Sample.lease_connection
156+
157+
Sample.transaction do
158+
@connection.materialize_transactions
159+
# Double check that the INSERT isn't seen with default isolation level
160+
assert_no_difference -> { Sample.count } do
161+
Thread.new do
162+
Sample.create!(value: 1)
163+
end.join
164+
end
165+
end
166+
167+
Sample.transaction(isolation: :read_committed) do
168+
@connection.materialize_transactions
169+
# Double check that the INSERT is seen with :read_committed
170+
assert_difference -> { Sample.count }, +1 do
171+
Thread.new do
172+
Sample.create!(value: 1)
173+
end.join
174+
end
175+
end
176+
177+
first_begin_failed = false
178+
@connection.singleton_class.define_method(:perform_query) do |raw_connection, sql, *args, **kwargs|
179+
# Simulates the first BEGIN attempt failing
180+
if sql.include?("BEGIN") && !first_begin_failed
181+
first_begin_failed = true
182+
raise ActiveRecord::ConnectionFailed, "Simulated failure"
183+
end
184+
super(raw_connection, sql, *args, **kwargs)
185+
end
186+
187+
Sample.transaction(isolation: :read_committed) do
188+
@connection.materialize_transactions
189+
# Indirectly check that the retried transaction is READ COMMITTED
190+
assert_difference -> { Sample.count }, +1 do
191+
Thread.new do
192+
Sample.create!(value: 1)
193+
end.join
194+
end
195+
end
196+
197+
assert first_begin_failed
198+
ensure
199+
pool.remove(@connection) # We're monkey patching the instance, we shouldn't re-use it
200+
@connection&.disconnect!
201+
Sample.release_connection
202+
end
152203
end
153204
end

0 commit comments

Comments
 (0)