1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Finalize transaction record state after real transaction

After a real (non-savepoint) transaction has committed or rolled back,
the original persistence-related state for all records modified in that
transaction is discarded or restored, respectively.

When the model has transactional callbacks, this happens synchronously
in the `committed!` or `rolled_back!` methods; otherwise, it happens
lazily the next time the record's persistence-related state is accessed.

The synchronous code path always finalizes the state of the record, but
the lazy code path only pops one "level" from the transaction counter,
assuming it will always reach zero immediately after a real transaction.
As the test cases included here demonstrate, that isn't always the case.

By using the same logic as the synchronous code path, we ensure that the
record's state is always updated after a real transaction has finished.
This commit is contained in:
Eugene Kenny 2018-05-16 22:40:20 +01:00
parent 1efbc634b5
commit 5359428a14
3 changed files with 54 additions and 15 deletions

View file

@ -17,11 +17,19 @@ module ActiveRecord
end
def committed?
@state == :committed
@state == :committed || @state == :fully_committed
end
def fully_committed?
@state == :fully_committed
end
def rolledback?
@state == :rolledback
@state == :rolledback || @state == :fully_rolledback
end
def fully_rolledback?
@state == :fully_rolledback
end
def fully_completed?
@ -55,10 +63,19 @@ module ActiveRecord
@state = :rolledback
end
def full_rollback!
@children.each { |c| c.rollback! }
@state = :fully_rolledback
end
def commit!
@state = :committed
end
def full_commit!
@state = :fully_committed
end
def nullify!
@state = nil
end
@ -88,10 +105,6 @@ module ActiveRecord
records << record
end
def rollback
@state.rollback!
end
def rollback_records
ite = records.uniq
while record = ite.shift
@ -103,10 +116,6 @@ module ActiveRecord
end
end
def commit
@state.commit!
end
def before_commit_records
records.uniq.each(&:before_committed!) if @run_commit_callbacks
end
@ -145,12 +154,12 @@ module ActiveRecord
def rollback
connection.rollback_to_savepoint(savepoint_name)
super
@state.rollback!
end
def commit
connection.release_savepoint(savepoint_name)
super
@state.commit!
end
def full_rollback?; false; end
@ -168,12 +177,12 @@ module ActiveRecord
def rollback
connection.rollback_db_transaction
super
@state.full_rollback!
end
def commit
connection.commit_db_transaction
super
@state.full_commit!
end
end

View file

@ -472,7 +472,8 @@ module ActiveRecord
def update_attributes_from_transaction_state(transaction_state)
if transaction_state && transaction_state.finalized?
restore_transaction_record_state if transaction_state.rolledback?
restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
force_clear_transaction_record_state if transaction_state.fully_committed?
clear_transaction_record_state if transaction_state.fully_completed?
end
end

View file

@ -679,6 +679,35 @@ class TransactionTest < ActiveRecord::TestCase
assert_not_predicate topic, :frozen?
end
def test_restore_new_record_after_double_save
topic = Topic.new
Topic.transaction do
topic.save!
topic.save!
raise ActiveRecord::Rollback
end
assert_predicate topic, :new_record?
end
def test_dont_restore_new_record_in_subsequent_transaction
topic = Topic.new
Topic.transaction do
topic.save!
topic.save!
end
Topic.transaction do
topic.save!
raise ActiveRecord::Rollback
end
assert_predicate topic, :persisted?
assert_not_predicate topic, :new_record?
end
def test_restore_id_after_rollback
topic = Topic.new