Skip to content

Commit 39a192f

Browse files
authored
Merge pull request rails#44607 from matthewd/mark-written-v2
Re-deprecate return/throw exits from transactions
2 parents a2217ae + 50db410 commit 39a192f

File tree

8 files changed

+105
-12
lines changed

8 files changed

+105
-12
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ def transaction(requires_new: nil, isolation: nil, joinable: true, &block)
326326
:disable_lazy_transactions!, :enable_lazy_transactions!, :dirty_current_transaction,
327327
to: :transaction_manager
328328

329+
def mark_transaction_written_if_write(sql) # :nodoc:
330+
transaction = current_transaction
331+
if transaction.open?
332+
transaction.written ||= write_query?(sql)
333+
end
334+
end
335+
329336
def transaction_open?
330337
current_transaction.open?
331338
end

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

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def dirty!; end
8787

8888
class Transaction # :nodoc:
8989
attr_reader :connection, :state, :savepoint_name, :isolation_level
90+
attr_accessor :written, :written_indirectly
9091

9192
def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
9293
@connection = connection
@@ -383,6 +384,10 @@ def commit_transaction
383384

384385
dirty_current_transaction if transaction.dirty?
385386

387+
if current_transaction.open?
388+
current_transaction.written_indirectly ||= transaction.written || transaction.written_indirectly
389+
end
390+
386391
transaction.commit
387392
transaction.commit_records
388393
end
@@ -419,19 +424,37 @@ def within_new_transaction(isolation: nil, joinable: true)
419424
# @connection still holds an open or invalid transaction, so we must not
420425
# put it back in the pool for reuse.
421426
@connection.throw_away! unless transaction.state.rolledback?
422-
elsif Thread.current.status == "aborting" || !completed
423-
# The transaction is still open but the block returned earlier.
424-
#
425-
# The block could return early because of a timeout or because the thread is aborting,
426-
# so we are rolling back to make sure the timeout didn't caused the transaction to be
427-
# committed incompletely.
428-
rollback_transaction
429427
else
430-
begin
431-
commit_transaction
432-
rescue Exception
433-
rollback_transaction(transaction) unless transaction.state.completed?
434-
raise
428+
if Thread.current.status == "aborting"
429+
rollback_transaction
430+
elsif !completed && transaction.written
431+
# This was deprecated in 6.1, and has now changed to a rollback
432+
rollback_transaction
433+
elsif !completed && !transaction.written_indirectly
434+
# This was a silent commit in 6.1, but now becomes a rollback; we skipped
435+
# the warning because (having not been written) the change generally won't
436+
# have any effect
437+
rollback_transaction
438+
else
439+
if !completed && transaction.written_indirectly
440+
# This is the case that was missed in the 6.1 deprecation, so we have to
441+
# do it now
442+
ActiveSupport::Deprecation.warn(<<~EOW)
443+
Using `return`, `break` or `throw` to exit a transaction block is
444+
deprecated without replacement. If the `throw` came from
445+
`Timeout.timeout(duration)`, pass an exception class as a second
446+
argument so it doesn't use `throw` to abort its block. This results
447+
in the transaction being committed, but in the next release of Rails
448+
it will rollback.
449+
EOW
450+
end
451+
452+
begin
453+
commit_transaction
454+
rescue Exception
455+
rollback_transaction(transaction) unless transaction.state.completed?
456+
raise
457+
end
435458
end
436459
end
437460
end

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ def extended_type_map_key
644644

645645
def raw_execute(sql, name, async: false)
646646
materialize_transactions
647+
mark_transaction_written_if_write(sql)
647648

648649
log(sql, name, async: async) do
649650
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def exec_stmt_and_free(sql, name, binds, cache_stmt: false, async: false)
168168
check_if_write_query(sql)
169169

170170
materialize_transactions
171+
mark_transaction_written_if_write(sql)
171172

172173
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
173174
# made since we established the connection

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def explain(arel, binds = [])
1212
# Queries the database and returns the results in an Array-like object
1313
def query(sql, name = nil) # :nodoc:
1414
materialize_transactions
15+
mark_transaction_written_if_write(sql)
1516

1617
log(sql, name) do
1718
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -40,6 +41,7 @@ def execute(sql, name = nil)
4041
check_if_write_query(sql)
4142

4243
materialize_transactions
44+
mark_transaction_written_if_write(sql)
4345

4446
log(sql, name) do
4547
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,7 @@ def execute_and_clear(sql, name, binds, prepare: false, async: false)
760760

761761
def exec_no_cache(sql, name, binds, async: false)
762762
materialize_transactions
763+
mark_transaction_written_if_write(sql)
763764

764765
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
765766
# made since we established the connection
@@ -775,6 +776,7 @@ def exec_no_cache(sql, name, binds, async: false)
775776

776777
def exec_cache(sql, name, binds, async: false)
777778
materialize_transactions
779+
mark_transaction_written_if_write(sql)
778780
update_typemap_for_default_timezone
779781

780782
stmt_key = prepare_statement(sql, binds)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def execute(sql, name = nil) # :nodoc:
2525
check_if_write_query(sql)
2626

2727
materialize_transactions
28+
mark_transaction_written_if_write(sql)
2829

2930
log(sql, name) do
3031
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -38,6 +39,7 @@ def exec_query(sql, name = nil, binds = [], prepare: false, async: false) # :nod
3839
check_if_write_query(sql)
3940

4041
materialize_transactions
42+
mark_transaction_written_if_write(sql)
4143

4244
type_casted_binds = type_casted_binds(binds)
4345

@@ -121,6 +123,7 @@ def execute_batch(statements, name = nil)
121123
check_if_write_query(sql)
122124

123125
materialize_transactions
126+
mark_transaction_written_if_write(sql)
124127

125128
log(sql, name) do
126129
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do

activerecord/test/cases/transactions_test.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,65 @@ def transaction_with_return
183183
end
184184
end
185185

186+
def transaction_with_shallow_return
187+
Topic.transaction do
188+
Topic.transaction(requires_new: true) do
189+
@first.approved = true
190+
@second.approved = false
191+
@first.save
192+
@second.save
193+
end
194+
return
195+
end
196+
end
197+
186198
def test_add_to_null_transaction
187199
topic = Topic.new
188200
topic.send(:add_to_transaction)
189201
end
190202

203+
def test_successful_with_return_outside_inner_transaction
204+
committed = false
205+
206+
Topic.connection.class_eval do
207+
alias :real_commit_db_transaction :commit_db_transaction
208+
define_method(:commit_db_transaction) do
209+
committed = true
210+
real_commit_db_transaction
211+
end
212+
end
213+
214+
assert_deprecated do
215+
transaction_with_shallow_return
216+
end
217+
assert committed
218+
219+
assert_predicate Topic.find(1), :approved?, "First should have been approved"
220+
assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
221+
ensure
222+
Topic.connection.class_eval do
223+
remove_method :commit_db_transaction
224+
alias :commit_db_transaction :real_commit_db_transaction rescue nil
225+
end
226+
end
227+
228+
def test_deprecation_on_ruby_timeout_outside_inner_transaction
229+
assert_deprecated do
230+
catch do |timeout|
231+
Topic.transaction do
232+
Topic.transaction(requires_new: true) do
233+
@first.approved = true
234+
@first.save!
235+
end
236+
237+
throw timeout
238+
end
239+
end
240+
end
241+
242+
assert Topic.find(1).approved?, "First should have been approved"
243+
end
244+
191245
def test_rollback_with_return
192246
committed = false
193247

0 commit comments

Comments
 (0)