Skip to content

Commit 50db410

Browse files
committed
Re-deprecate return/throw exits from transactions
We previously weren't warning about changes written in a nested transaction.
1 parent 46339b2 commit 50db410

File tree

2 files changed

+89
-13
lines changed

2 files changed

+89
-13
lines changed

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

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

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

9292
def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
9393
@connection = connection
@@ -384,6 +384,10 @@ def commit_transaction
384384

385385
dirty_current_transaction if transaction.dirty?
386386

387+
if current_transaction.open?
388+
current_transaction.written_indirectly ||= transaction.written || transaction.written_indirectly
389+
end
390+
387391
transaction.commit
388392
transaction.commit_records
389393
end
@@ -420,19 +424,37 @@ def within_new_transaction(isolation: nil, joinable: true)
420424
# @connection still holds an open or invalid transaction, so we must not
421425
# put it back in the pool for reuse.
422426
@connection.throw_away! unless transaction.state.rolledback?
423-
elsif Thread.current.status == "aborting" || (!completed && transaction.written)
424-
# The transaction is still open but the block returned earlier.
425-
#
426-
# The block could return early because of a timeout or because the thread is aborting,
427-
# so we are rolling back to make sure the timeout didn't caused the transaction to be
428-
# committed incompletely.
429-
rollback_transaction
430427
else
431-
begin
432-
commit_transaction
433-
rescue Exception
434-
rollback_transaction(transaction) unless transaction.state.completed?
435-
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
436458
end
437459
end
438460
end

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)