diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract/transaction.rb')
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/abstract/transaction.rb | 230 |
1 files changed, 142 insertions, 88 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 4cca94e40b..fd666c8c39 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,128 +1,143 @@ module ActiveRecord module ConnectionAdapters - class Transaction #:nodoc: - attr_reader :connection + class TransactionState + attr_reader :parent - def initialize(connection) - @connection = connection - end - end + VALID_STATES = Set.new([:committed, :rolledback, nil]) - class ClosedTransaction < Transaction #:nodoc: - def number - 0 + def initialize(state = nil) + @state = state + @parent = nil end - def begin(options = {}) - RealTransaction.new(connection, self, options) + def finalized? + @state end - def closed? - true + def committed? + @state == :committed end - def open? - false + def rolledback? + @state == :rolledback end - def joinable? - false + def completed? + committed? || rolledback? end - # This is a noop when there are no open transactions - def add_record(record) + def set_state(state) + if !VALID_STATES.include?(state) + raise ArgumentError, "Invalid transaction state: #{state}" + end + @state = state end end - class OpenTransaction < Transaction #:nodoc: - attr_reader :parent, :records - attr_writer :joinable - - def initialize(connection, parent, options = {}) - super connection - - @parent = parent - @records = [] - @finishing = false - @joinable = options.fetch(:joinable, true) - end + class NullTransaction #:nodoc: + def initialize; end + def closed?; true; end + def open?; false; end + def joinable?; false; end + def add_record(record); end + end - # This state is necesarry 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 + class Transaction #:nodoc: - def joinable? - @joinable && !finishing? - end + attr_reader :connection, :state, :records, :savepoint_name + attr_writer :joinable - def number - if finishing? - parent.number - else - parent.number + 1 - end + def initialize(connection, options) + @connection = connection + @state = TransactionState.new + @records = [] + @joinable = options.fetch(:joinable, true) end - def begin(options = {}) - if finishing? - parent.begin + def add_record(record) + if record.has_transactional_callbacks? + records << record else - SavepointTransaction.new(connection, self, options) + record.set_transaction_state(@state) end end def rollback - @finishing = true - perform_rollback - parent - end - - def commit - @finishing = true - perform_commit - parent - end - - def add_record(record) - records << record + @state.set_state(:rolledback) end def rollback_records - records.uniq.each do |record| + ite = records.uniq + while record = ite.shift begin - record.rolledback!(parent.closed?) + record.rolledback! full_rollback? rescue => e + raise if ActiveRecord::Base.raise_in_transactional_callbacks record.logger.error(e) if record.respond_to?(:logger) && record.logger end end + ensure + ite.each do |i| + i.rolledback!(full_rollback?, false) + end + end + + def commit + @state.set_state(:committed) end def commit_records - records.uniq.each do |record| + ite = records.uniq + while record = ite.shift begin record.committed! rescue => e + raise if ActiveRecord::Base.raise_in_transactional_callbacks record.logger.error(e) if record.respond_to?(:logger) && record.logger end end + ensure + ite.each do |i| + i.committed!(false) + end end - def closed? - false + def full_rollback?; true; end + def joinable?; @joinable; end + def closed?; false; end + def open?; !closed?; end + end + + class SavepointTransaction < Transaction + + def initialize(connection, savepoint_name, options) + super(connection, options) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + connection.create_savepoint(@savepoint_name = savepoint_name) end - def open? - true + def rollback + connection.rollback_to_savepoint(savepoint_name) + super + rollback_records end - end - class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) + def commit + connection.release_savepoint(savepoint_name) super + parent = connection.transaction_manager.current_transaction + records.each { |r| parent.add_record(r) } + end + + def full_rollback?; false; end + end + + class RealTransaction < Transaction + def initialize(connection, options) + super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) else @@ -130,36 +145,75 @@ module ActiveRecord end end - def perform_rollback + def rollback connection.rollback_db_transaction + super rollback_records end - def perform_commit + def commit connection.commit_db_transaction + super commit_records end end - class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - if options[:isolation] - raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" - end + class TransactionManager #:nodoc: + def initialize(connection) + @stack = [] + @connection = connection + end - super - connection.create_savepoint + def begin_transaction(options = {}) + transaction = + if @stack.empty? + RealTransaction.new(@connection, options) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", 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 + rollback_transaction if transaction + raise + ensure + unless error + if Thread.current.status == 'aborting' + rollback_transaction + else + begin + commit_transaction + rescue Exception + transaction.rollback unless transaction.state.completed? + raise + end + end + end end - def perform_rollback - connection.rollback_to_savepoint - rollback_records + def open_transactions + @stack.size end - def perform_commit - connection.release_savepoint - records.each { |r| parent.add_record(r) } + def current_transaction + @stack.last || NULL_TRANSACTION end + + private + NULL_TRANSACTION = NullTransaction.new end end end |