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

Merge pull request #16284 from arthurnn/transactions

Transactions refactoring
This commit is contained in:
Rafael Mendonça França 2014-07-28 15:02:28 -03:00
commit 6501aeb286
4 changed files with 101 additions and 62 deletions

View file

@ -203,62 +203,30 @@ module ActiveRecord
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
yield
else
within_new_transaction(options) { yield }
transaction_manager.within_new_transaction(options) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
end
def within_new_transaction(options = {}) #:nodoc:
transaction = begin_transaction(options)
yield
rescue Exception => error
rollback_transaction if transaction
raise
ensure
begin
commit_transaction unless error
rescue Exception
rollback_transaction
raise
end
end
attr_reader :transaction_manager #:nodoc:
def open_transactions
@transaction.number
end
def current_transaction #:nodoc:
@transaction
end
delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
def transaction_open?
@transaction.open?
end
def begin_transaction(options = {}) #:nodoc:
@transaction = @transaction.begin(options)
end
def commit_transaction #:nodoc:
@transaction = @transaction.commit
end
def rollback_transaction #:nodoc:
@transaction = @transaction.rollback
current_transaction.open?
end
def reset_transaction #:nodoc:
@transaction = ClosedTransaction.new(self)
@transaction_manager = TransactionManager.new(self)
end
# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
@transaction.add_record(record)
current_transaction.add_record(record)
end
# Begins the transaction (and turns off auto-committing).

View file

@ -1,5 +1,63 @@
module ActiveRecord
module ConnectionAdapters
class TransactionManager #:nodoc:
def initialize(connection)
@stack = []
@connection = connection
end
def begin_transaction(options = {})
transaction =
if @stack.empty?
RealTransaction.new(@connection, current_transaction, options)
else
SavepointTransaction.new(@connection, current_transaction, options)
end
@stack.push(transaction)
transaction
end
def commit_transaction
@stack.pop.commit
end
def rollback_transaction
@stack.pop.rollback
end
def within_new_transaction(options = {})
transaction = begin_transaction options
yield
rescue Exception => error
transaction.rollback if transaction
raise
ensure
begin
transaction.commit unless error
rescue Exception
transaction.rollback
raise
ensure
@stack.pop if transaction
end
end
def open_transactions
@stack.size
end
def current_transaction
@stack.last || closed_transaction
end
private
def closed_transaction
@closed_transaction ||= ClosedTransaction.new(@connection)
end
end
class Transaction #:nodoc:
attr_reader :connection
@ -11,6 +69,10 @@ module ActiveRecord
def state
@state
end
def savepoint_name
nil
end
end
class TransactionState
@ -78,45 +140,28 @@ module ActiveRecord
@parent = parent
@records = []
@finishing = false
@joinable = options.fetch(:joinable, true)
end
# This state is necessary so that we correctly handle stuff that might
# happen in a commit/rollback. But it's kinda distasteful. Maybe we can
# find a better way to structure it in the future.
def finishing?
@finishing
end
def joinable?
@joinable && !finishing?
@joinable
end
def number
if finishing?
parent.number
else
parent.number + 1
end
parent.number + 1
end
def begin(options = {})
if finishing?
parent.begin
else
SavepointTransaction.new(connection, self, options)
end
SavepointTransaction.new(connection, self, options)
end
def rollback
@finishing = true
perform_rollback
parent
end
def commit
@finishing = true
perform_commit
parent
end
@ -183,24 +228,29 @@ module ActiveRecord
end
class SavepointTransaction < OpenTransaction #:nodoc:
attr_reader :savepoint_name
def initialize(connection, parent, options = {})
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
end
super
connection.create_savepoint
# Savepoint name only counts the Savepoint transactions, so we need to subtract 1
@savepoint_name = "active_record_#{number - 1}"
connection.create_savepoint(@savepoint_name)
end
def perform_rollback
connection.rollback_to_savepoint
connection.rollback_to_savepoint(@savepoint_name)
rollback_records
end
def perform_commit
@state.set_state(:committed)
@state.parent = parent.state
connection.release_savepoint
connection.release_savepoint(@savepoint_name)
end
end
end

View file

@ -45,6 +45,7 @@ module ActiveRecord
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
autoload :TransactionManager
autoload :ClosedTransaction
autoload :RealTransaction
autoload :SavepointTransaction
@ -357,7 +358,7 @@ module ActiveRecord
end
def current_savepoint_name
"active_record_#{open_transactions}"
current_transaction.savepoint_name
end
# Check the connection back in to the connection pool

View file

@ -424,6 +424,26 @@ class TransactionTest < ActiveRecord::TestCase
end
end
def test_savepoints_name
Topic.transaction do
assert_nil Topic.connection.current_savepoint_name
assert_nil Topic.connection.current_transaction.savepoint_name
Topic.transaction(requires_new: true) do
assert_equal "active_record_1", Topic.connection.current_savepoint_name
assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
Topic.transaction(requires_new: true) do
assert_equal "active_record_2", Topic.connection.current_savepoint_name
assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name
end
assert_equal "active_record_1", Topic.connection.current_savepoint_name
assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
end
end
end
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')