Thursday, May 4, 2017

Working Around ActiveRecord PostgreSQL Savepoint Problems (RELEASE SAVEPOINT active_record_1)

Ended up not using it, so putting it here if someone runs into problems with AR 4.x savepoints.

The error:

ActiveRecord::StatementInvalid:         ActiveRecord::StatementInvalid: PG::InFailedSqlTransaction: ERROR:  current transaction is aborted, commands ignored until end of transaction block
        : RELEASE SAVEPOINT active_record_1
            .../gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:155:in `async_exec'
            .../gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:155:in `block in execute'
            .../gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract_adapter.rb:484:in `block in log'
            .../gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:20:in `instrument'
            .../gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract_adapter.rb:478:in `log'
            .../gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:154:in `execute'
            ...

The fix:

module SavepointsFix
  class << self
    attr_accessor :enabled, :debug, :active_savepoints, :rollback_on_fail

    def get_backtrace
      begin
        raise
      rescue => e
        e.backtrace.join("\n")
      end
    end

  end

  def create_savepoint(name = current_savepoint_name)
    if SavepointsFix.enabled
      begin
        puts "SAVEPOINT #{name}. call stack:\n#{SavepointsFix.get_backtrace}\n\n" if SavepointsFix.debug
        result = super(name)
        SavepointsFix.active_savepoints.push name
        result
      rescue Exception => e
        puts "#{e.message}\n#{e.backtrace.join("\n  ")}" if SavepointsFix.debug
        if SavepointsFix.rollback_on_fail
          begin
            ActiveRecord::Base.connection.execute 'ROLLBACK'
            active_savepoints = []
          rescue => e
            puts "#{e.message}\n#{e.backtrace.join("\n  ")}" if SavepointsFix.debug
          end
        end
      else
      end
    else
      super(name)
    end
  end

  def exec_rollback_to_savepoint(name = current_savepoint_name)
    if SavepointsFix.enabled
      if SavepointsFix.active_savepoints.length > 0 && SavepointsFix.active_savepoints.last == name
        puts "ROLLBACK TO SAVEPOINT #{name}. call stack:\n#{SavepointsFix.get_backtrace}\n\n" if SavepointsFix.debug
        begin
          result = super(name)
          SavepointsFix.active_savepoints.pop
          result
        rescue Exception => e
          puts "#{e.message}\n#{e.backtrace.join("\n  ")}" if SavepointsFix.debug
          if SavepointsFix.rollback_on_fail
            begin
              ActiveRecord::Base.connection.execute 'ROLLBACK'
              active_savepoints = []
            rescue => e
              puts "#{e.message}\n#{e.backtrace.join("\n  ")}" if SavepointsFix.debug
            end
          end
        end
      else
        puts "IGNORED EXTRA: ROLLBACK TO SAVEPOINT #{name}. call stack:\n#{SavepointsFix.get_backtrace}\n\n" if SavepointsFix.debug
      end
    else
      super(name)
    end
  end

  def release_savepoint(name = current_savepoint_name)
    if SavepointsFix.enabled
      if SavepointsFix.active_savepoints.length > 0 && SavepointsFix.active_savepoints.last == name
        puts "RELEASE SAVEPOINT #{name}. call stack:\n#{SavepointsFix.get_backtrace}\n\n" if SavepointsFix.debug
        begin
          result = super(name)
          SavepointsFix.active_savepoints.pop
          result
        rescue Exception => e
          puts "#{e.message}\n#{e.backtrace.join("\n  ")}" if SavepointsFix.debug
          if SavepointsFix.rollback_on_fail
            begin
              ActiveRecord::Base.connection.execute 'ROLLBACK'
              active_savepoints = []
            rescue => e
              puts "#{e.message}\n#{e.backtrace.join("\n  ")}" if SavepointsFix.debug
            end
          end
        end
      else
        puts "IGNORED EXTRA: RELEASE SAVEPOINT #{name}. call stack:\n#{SavepointsFix.get_backtrace}\n\n" if SavepointsFix.debug
      end
    else
      super(name)
    end
  end
end

SavepointsFix.active_savepoints = []
SavepointsFix.debug = false
SavepointsFix.rollback_on_fail = true

::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend SavepointsFix

class Minitest::Test
  alias_method :run_with_savepoint_behavior, :run

  def run
    SavepointsFix.debug = false
    result = run_with_savepoint_behavior    
    result
  ensure
    SavepointsFix.debug = false
  end

  def debug_savepoints(&blk)
    SavepointsFix.debug = true
    yield
  ensure
    SavepointsFix.debug = false
  end

  def with_savepoints_fix(&blk)
    SavepointsFix.enabled = true
    yield
  ensure
    SavepointsFix.enabled = false
  end
end

Usage in a minitest test:

with_savepoints_fix do
  some_model.destroy!
end

No comments: