# 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
begin
transaction = begin_transaction options
yield
rescue Exception => error
if transaction
rollback_transaction
after_failure_actions(transaction, error)
end
raise
ensure
unless error
if Thread.current.status == "aborting"
rollback_transaction if transaction
else
begin
commit_transaction if transaction
rescue Exception
rollback_transaction(transaction) unless transaction.state.completed?
raise
end
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