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 | 330 |
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 |