Simplify the implementation by only calling the checks in the right places

This commit is contained in:
Rafael Mendonça França 2021-06-16 21:17:34 +00:00
parent d9f2453827
commit 115d4a3994
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
5 changed files with 61 additions and 73 deletions

View File

@ -524,6 +524,7 @@ GEM
zeitwerk (2.4.2) zeitwerk (2.4.2)
PLATFORMS PLATFORMS
ruby
x86_64-darwin-19 x86_64-darwin-19
x86_64-darwin-20 x86_64-darwin-20
x86_64-linux x86_64-linux

View File

@ -1,3 +1,7 @@
* Do not try to rollback transactions that failed due to a `ActiveRecord::TransactionRollbackError`.
*Jamie McCarthy*
* Active Record Encryption will now encode values as UTF-8 when using deterministic * Active Record Encryption will now encode values as UTF-8 when using deterministic
encryption. The encoding is part of the encrypted payload, so different encodings for encryption. The encoding is part of the encrypted payload, so different encodings for
different values result in different ciphertexts. This can break unique constraints and different values result in different ciphertexts. This can break unique constraints and

View File

@ -37,10 +37,6 @@ module ActiveRecord
@state == :invalidated @state == :invalidated
end end
def uncommittable?
invalidated? || rolledback?
end
def fully_completed? def fully_completed?
completed? completed?
end end
@ -59,6 +55,11 @@ module ActiveRecord
@state = :fully_rolledback @state = :fully_rolledback
end end
def invalidate!
@children&.each { |c| c.invalidate! }
@state = :invalidated
end
def commit! def commit!
@state = :committed @state = :committed
end end
@ -67,11 +68,6 @@ module ActiveRecord
@state = :fully_committed @state = :fully_committed
end end
def invalidate!
@children.each { |c| c.invalidate! }
@state = :invalidated
end
def nullify! def nullify!
@state = nil @state = nil
end end
@ -166,10 +162,6 @@ module ActiveRecord
ite&.each { |i| i.committed!(should_run_callbacks: false) } ite&.each { |i| i.committed!(should_run_callbacks: false) }
end end
def invalidate
@state.invalidate!
end
def full_rollback?; true; end def full_rollback?; true; end
def joinable?; @joinable; end def joinable?; @joinable; end
def closed?; false; end def closed?; false; end
@ -195,14 +187,11 @@ module ActiveRecord
end end
def rollback def rollback
connection.rollback_to_savepoint(savepoint_name) if materialized? && !state.invalidated? connection.rollback_to_savepoint(savepoint_name) if materialized?
@state.rollback! @state.rollback!
end end
def commit def commit
if state.invalidated?
raise ActiveRecord::StatementInvalid, "cannot commit after transaction invalidated"
end
connection.release_savepoint(savepoint_name) if materialized? connection.release_savepoint(savepoint_name) if materialized?
@state.commit! @state.commit!
end end
@ -222,14 +211,11 @@ module ActiveRecord
end end
def rollback def rollback
connection.rollback_db_transaction if materialized? && !state.invalidated? connection.rollback_db_transaction if materialized?
@state.full_rollback! @state.full_rollback!
end end
def commit def commit
if state.invalidated?
raise ActiveRecord::StatementInvalid, "cannot commit after transaction invalidated"
end
connection.commit_db_transaction if materialized? connection.commit_db_transaction if materialized?
@state.full_commit! @state.full_commit!
end end
@ -307,9 +293,6 @@ module ActiveRecord
def commit_transaction def commit_transaction
@connection.lock.synchronize do @connection.lock.synchronize do
transaction = @stack.last transaction = @stack.last
if transaction.state.invalidated?
raise ActiveRecord::StatementInvalid, "cannot commit after transaction invalidated"
end
begin begin
transaction.before_commit_records transaction.before_commit_records
@ -342,12 +325,13 @@ module ActiveRecord
rollback_transaction rollback_transaction
after_failure_actions(transaction, error) after_failure_actions(transaction, error)
end end
raise raise
ensure ensure
if transaction if transaction
if error if error
# @connection still holds an open transaction, so we must not # @connection still holds an open or invalid transaction, so we must not
# put it back in the pool for reuse # put it back in the pool for reuse.
@connection.throw_away! unless transaction.state.rolledback? @connection.throw_away! unless transaction.state.rolledback?
else else
if Thread.current.status == "aborting" if Thread.current.status == "aborting"

View File

@ -13,12 +13,12 @@ module ActiveRecord
setup do setup do
@abort, Thread.abort_on_exception = Thread.abort_on_exception, false @abort, Thread.abort_on_exception = Thread.abort_on_exception, false
Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
@connection = ActiveRecord::Base.connection @connection = ActiveRecord::Base.connection
@connection.clear_cache! @connection.clear_cache!
@connection.drop_table "samples", if_exists: true @connection.create_table("samples", force: true) do |t|
@connection.create_table("samples") do |t|
t.integer "value" t.integer "value"
end end
@ -30,6 +30,7 @@ module ActiveRecord
@connection.drop_table "samples", if_exists: true @connection.drop_table "samples", if_exists: true
Thread.abort_on_exception = @abort Thread.abort_on_exception = @abort
Thread.report_on_exception = @original_report_on_exception
end end
test "deadlock correctly raises Deadlocked inside nested SavepointTransaction" do test "deadlock correctly raises Deadlocked inside nested SavepointTransaction" do
@ -45,7 +46,7 @@ module ActiveRecord
Sample.transaction(requires_new: true) do Sample.transaction(requires_new: true) do
s1.lock! s1.lock!
barrier.wait barrier.wait
s2.update_attributes value: 1 s2.update value: 1
end end
end end
end end
@ -55,16 +56,15 @@ module ActiveRecord
Sample.transaction(requires_new: true) do Sample.transaction(requires_new: true) do
s2.lock! s2.lock!
barrier.wait barrier.wait
s1.update_attributes value: 2 s1.update value: 2
end end
end end
ensure ensure
thread.join thread.join
end end
rescue ActiveRecord::StatementInvalid => e rescue ActiveRecord::StatementInvalid => e
if /SAVEPOINT active_record_. does not exist: ROLLBACK TO SAVEPOINT/ =~ e.to_s if /SAVEPOINT active_record_. does not exist/ =~ e.to_s
assert nil, "ROLLBACK TO SAVEPOINT query issued for savepoint that no longer exists due to deadlock: #{e}" flunk "ROLLBACK TO SAVEPOINT query issued for savepoint that no longer exists due to deadlock: #{e}"
else else
raise e raise e
end end

View File

@ -14,12 +14,15 @@ module ActiveRecord
setup do setup do
@abort, Thread.abort_on_exception = Thread.abort_on_exception, false @abort, Thread.abort_on_exception = Thread.abort_on_exception, false
Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
@connection = ActiveRecord::Base.connection @connection = ActiveRecord::Base.connection
@connection.drop_table "samples", if_exists: true @connection.transaction do
@connection.create_table("samples") do |t| @connection.drop_table "samples", if_exists: true
t.integer "value" @connection.create_table("samples") do |t|
t.integer "value"
end
end end
Sample.reset_column_information Sample.reset_column_information
@ -29,6 +32,7 @@ module ActiveRecord
@connection.drop_table "samples", if_exists: true @connection.drop_table "samples", if_exists: true
Thread.abort_on_exception = @abort Thread.abort_on_exception = @abort
Thread.report_on_exception = @original_report_on_exception
end end
test "unserializable transaction raises SerializationFailure inside nested SavepointTransaction" do test "unserializable transaction raises SerializationFailure inside nested SavepointTransaction" do
@ -36,32 +40,30 @@ module ActiveRecord
before = Concurrent::CyclicBarrier.new(2) before = Concurrent::CyclicBarrier.new(2)
after = Concurrent::CyclicBarrier.new(2) after = Concurrent::CyclicBarrier.new(2)
begin thread = Thread.new do
thread = Thread.new do with_warning_suppression do
with_warning_suppression do Sample.transaction(isolation: :serializable, requires_new: false) do
Sample.transaction(isolation: :serializable, requires_new: false) do Sample.transaction(requires_new: true) do
Sample.transaction(requires_new: true) do before.wait
before.wait Sample.create value: Sample.sum(:value)
Sample.create value: Sample.sum(:value) after.wait
after.wait
end
end end
end end
end end
end
begin begin
with_warning_suppression do with_warning_suppression do
Sample.transaction(isolation: :serializable, requires_new: false) do Sample.transaction(isolation: :serializable, requires_new: false) do
Sample.transaction(requires_new: true) do Sample.transaction(requires_new: true) do
before.wait before.wait
Sample.create value: Sample.sum(:value) Sample.create value: Sample.sum(:value)
after.wait after.wait
end
end end
end end
ensure
thread.join
end end
ensure
thread.join
end end
end end
end end
@ -74,35 +76,32 @@ module ActiveRecord
s1 = Sample.create value: 1 s1 = Sample.create value: 1
s2 = Sample.create value: 2 s2 = Sample.create value: 2
begin thread = Thread.new do
thread = Thread.new do Sample.transaction(requires_new: false) do
Sample.transaction(requires_new: false) do Sample.transaction(requires_new: true) do
Sample.transaction(requires_new: true) do s1.lock!
s1.lock! barrier.wait
barrier.wait s2.update value: 1
s2.update_attributes value: 1
end
end end
end end
end
begin begin
Sample.transaction(requires_new: false) do Sample.transaction(requires_new: false) do
Sample.transaction(requires_new: true) do Sample.transaction(requires_new: true) do
s2.lock! s2.lock!
barrier.wait barrier.wait
s1.update_attributes value: 2 s1.update value: 2
end
end end
ensure
thread.join
end end
ensure
thread.join
end end
end end
end end
end end
private private
def with_warning_suppression def with_warning_suppression
log_level = ActiveRecord::Base.connection.client_min_messages log_level = ActiveRecord::Base.connection.client_min_messages
ActiveRecord::Base.connection.client_min_messages = "error" ActiveRecord::Base.connection.client_min_messages = "error"