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

View file

@ -1,5 +1,63 @@
module ActiveRecord module ActiveRecord
module ConnectionAdapters 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: class Transaction #:nodoc:
attr_reader :connection attr_reader :connection
@ -11,6 +69,10 @@ module ActiveRecord
def state def state
@state @state
end end
def savepoint_name
nil
end
end end
class TransactionState class TransactionState
@ -78,45 +140,28 @@ module ActiveRecord
@parent = parent @parent = parent
@records = [] @records = []
@finishing = false
@joinable = options.fetch(:joinable, true) @joinable = options.fetch(:joinable, true)
end 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? def joinable?
@joinable && !finishing? @joinable
end end
def number def number
if finishing? parent.number + 1
parent.number
else
parent.number + 1
end
end end
def begin(options = {}) def begin(options = {})
if finishing? SavepointTransaction.new(connection, self, options)
parent.begin
else
SavepointTransaction.new(connection, self, options)
end
end end
def rollback def rollback
@finishing = true
perform_rollback perform_rollback
parent parent
end end
def commit def commit
@finishing = true
perform_commit perform_commit
parent parent
end end
@ -183,24 +228,29 @@ module ActiveRecord
end end
class SavepointTransaction < OpenTransaction #:nodoc: class SavepointTransaction < OpenTransaction #:nodoc:
attr_reader :savepoint_name
def initialize(connection, parent, options = {}) def initialize(connection, parent, options = {})
if options[:isolation] if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
end end
super 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 end
def perform_rollback def perform_rollback
connection.rollback_to_savepoint connection.rollback_to_savepoint(@savepoint_name)
rollback_records rollback_records
end end
def perform_commit def perform_commit
@state.set_state(:committed) @state.set_state(:committed)
@state.parent = parent.state @state.parent = parent.state
connection.release_savepoint connection.release_savepoint(@savepoint_name)
end end
end end
end end

View file

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

View file

@ -424,6 +424,26 @@ class TransactionTest < ActiveRecord::TestCase
end end
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 def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES') Topic.connection.expects(:commit_db_transaction).raises('OH NOES')