aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract/transaction.rb')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb330
1 files changed, 330 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
new file mode 100644
index 0000000000..718910b090
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -0,0 +1,330 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ class TransactionState
+ def initialize(state = nil)
+ @state = state
+ @children = []
+ end
+
+ def add_child(state)
+ @children << state
+ end
+
+ def finalized?
+ @state
+ end
+
+ def committed?
+ @state == :committed || @state == :fully_committed
+ end
+
+ def fully_committed?
+ @state == :fully_committed
+ end
+
+ def rolledback?
+ @state == :rolledback || @state == :fully_rolledback
+ end
+
+ def fully_rolledback?
+ @state == :fully_rolledback
+ end
+
+ def fully_completed?
+ completed?
+ end
+
+ def completed?
+ committed? || rolledback?
+ end
+
+ def set_state(state)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The set_state method is deprecated and will be removed in
+ Rails 6.0. Please use rollback! or commit! to set transaction
+ state directly.
+ MSG
+ case state
+ when :rolledback
+ rollback!
+ when :committed
+ commit!
+ when nil
+ nullify!
+ else
+ raise ArgumentError, "Invalid transaction state: #{state}"
+ end
+ end
+
+ def rollback!
+ @children.each { |c| c.rollback! }
+ @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
+ end
+
+ class NullTransaction #:nodoc:
+ def initialize; end
+ def state; end
+ def closed?; true; end
+ def open?; false; end
+ def joinable?; false; end
+ def add_record(record); end
+ end
+
+ class Transaction #:nodoc:
+ attr_reader :connection, :state, :records, :savepoint_name, :isolation_level
+
+ def initialize(connection, options, run_commit_callbacks: false)
+ @connection = connection
+ @state = TransactionState.new
+ @records = []
+ @isolation_level = options[:isolation]
+ @materialized = false
+ @joinable = options.fetch(:joinable, true)
+ @run_commit_callbacks = run_commit_callbacks
+ end
+
+ def add_record(record)
+ records << record
+ end
+
+ def materialize!
+ @materialized = true
+ end
+
+ def materialized?
+ @materialized
+ end
+
+ def rollback_records
+ ite = records.uniq
+ while record = ite.shift
+ record.rolledback!(force_restore_state: full_rollback?)
+ end
+ ensure
+ ite.each do |i|
+ i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
+ end
+ end
+
+ def before_commit_records
+ records.uniq.each(&:before_committed!) if @run_commit_callbacks
+ end
+
+ def commit_records
+ ite = records.uniq
+ while record = ite.shift
+ if @run_commit_callbacks
+ record.committed!
+ else
+ # if not running callbacks, only adds the record to the parent transaction
+ connection.add_transaction_record(record)
+ end
+ end
+ ensure
+ ite.each { |i| i.committed!(should_run_callbacks: false) }
+ end
+
+ 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, parent_transaction, *args)
+ super(connection, *args)
+
+ parent_transaction.state.add_child(@state)
+
+ if isolation_level
+ raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
+ end
+
+ @savepoint_name = savepoint_name
+ end
+
+ def materialize!
+ connection.create_savepoint(savepoint_name)
+ super
+ end
+
+ def rollback
+ connection.rollback_to_savepoint(savepoint_name) if materialized?
+ @state.rollback!
+ end
+
+ def commit
+ connection.release_savepoint(savepoint_name) if materialized?
+ @state.commit!
+ end
+
+ def full_rollback?; false; end
+ end
+
+ class RealTransaction < Transaction
+ def materialize!
+ if isolation_level
+ connection.begin_isolated_db_transaction(isolation_level)
+ else
+ connection.begin_db_transaction
+ end
+
+ super
+ end
+
+ def rollback
+ connection.rollback_db_transaction if materialized?
+ @state.full_rollback!
+ end
+
+ def commit
+ connection.commit_db_transaction if materialized?
+ @state.full_commit!
+ end
+ end
+
+ class TransactionManager #:nodoc:
+ def initialize(connection)
+ @stack = []
+ @connection = connection
+ @has_unmaterialized_transactions = false
+ @materializing_transactions = false
+ @lazy_transactions_enabled = true
+ end
+
+ def begin_transaction(options = {})
+ @connection.lock.synchronize do
+ run_commit_callbacks = !current_transaction.joinable?
+ transaction =
+ if @stack.empty?
+ RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
+ else
+ SavepointTransaction.new(@connection, "active_record_#{@stack.size}", @stack.last, options,
+ run_commit_callbacks: run_commit_callbacks)
+ end
+
+ transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled?
+ @stack.push(transaction)
+ @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions?
+ transaction
+ end
+ end
+
+ def disable_lazy_transactions!
+ materialize_transactions
+ @lazy_transactions_enabled = false
+ end
+
+ def enable_lazy_transactions!
+ @lazy_transactions_enabled = true
+ end
+
+ def lazy_transactions_enabled?
+ @lazy_transactions_enabled
+ end
+
+ def materialize_transactions
+ return if @materializing_transactions
+ return unless @has_unmaterialized_transactions
+
+ @connection.lock.synchronize do
+ begin
+ @materializing_transactions = true
+ @stack.each { |t| t.materialize! unless t.materialized? }
+ ensure
+ @materializing_transactions = false
+ end
+ @has_unmaterialized_transactions = false
+ end
+ end
+
+ def commit_transaction
+ @connection.lock.synchronize do
+ transaction = @stack.last
+
+ begin
+ transaction.before_commit_records
+ ensure
+ @stack.pop
+ end
+
+ transaction.commit
+ transaction.commit_records
+ end
+ end
+
+ def rollback_transaction(transaction = nil)
+ @connection.lock.synchronize do
+ transaction ||= @stack.pop
+ transaction.rollback
+ transaction.rollback_records
+ end
+ end
+
+ def within_new_transaction(options = {})
+ @connection.lock.synchronize do
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ if transaction
+ rollback_transaction
+ after_failure_actions(transaction, error)
+ end
+ raise
+ ensure
+ if !error && transaction
+ if Thread.current.status == "aborting"
+ rollback_transaction
+ else
+ begin
+ commit_transaction
+ rescue Exception
+ rollback_transaction(transaction) unless transaction.state.completed?
+ raise
+ end
+ end
+ end
+ end
+ end
+
+ def open_transactions
+ @stack.size
+ end
+
+ def current_transaction
+ @stack.last || NULL_TRANSACTION
+ end
+
+ private
+
+ NULL_TRANSACTION = NullTransaction.new
+
+ # Deallocate invalidated prepared statements outside of the transaction
+ def after_failure_actions(transaction, error)
+ return unless transaction.is_a?(RealTransaction)
+ return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired)
+ @connection.clear_cache!
+ end
+ end
+ end
+end