class OnlineMigrations::LockRetrier

This class provides a way to automatically retry code that relies on acquiring a database lock in a way designed to minimize impact on a busy production database.

This class defines an interface for child classes to implement to configure timing configurations and the maximum number of attempts.

There are two predefined implementations (see OnlineMigrations::ConstantLockRetrier and OnlineMigrations::ExponentialLockRetrier). It is easy to provide more sophisticated implementations.

@example Custom LockRetrier implementation

module OnlineMigrations
  class SophisticatedLockRetrier < LockRetrier
    TIMINGS = [
      [0.1.seconds, 0.05.seconds], # first - lock timeout, second - delay time
      [0.1.seconds, 0.05.seconds],
      [0.2.seconds, 0.05.seconds],
      [0.3.seconds, 0.10.seconds],
      [1.second, 5.seconds],
      [1.second, 1.minute],
      [0.1.seconds, 0.05.seconds],
      [0.2.seconds, 0.15.seconds],
      [0.5.seconds, 2.seconds],
      [0.5.seconds, 2.seconds],
      [3.seconds, 3.minutes],
      [0.1.seconds, 0.05.seconds],
      [0.5.seconds, 2.seconds],
      [5.seconds, 2.minutes],
      [7.seconds, 5.minutes],
      [0.5.seconds, 2.seconds],
    ]

    def attempts
      TIMINGS.size
    end

    def lock_timeout(attempt)
      TIMINGS[attempt - 1][0]
    end

    def delay(attempt)
      TIMINGS[attempt - 1][1]
    end
  end

Attributes

connection[RW]

Database connection on which retries are run

Public Instance Methods

attempts() click to toggle source

Returns the number of retrying attempts

# File lib/online_migrations/lock_retrier.rb, line 55
def attempts
  raise NotImplementedError
end
delay(_attempt) click to toggle source

Returns sleep time after unsuccessful lock attempt (in seconds)

@param _attempt [Integer] attempt number

# File lib/online_migrations/lock_retrier.rb, line 71
def delay(_attempt)
  raise NotImplementedError
end
lock_timeout(_attempt) click to toggle source

Returns database lock timeout value (in seconds) for specified attempt number

@param _attempt [Integer] attempt number

# File lib/online_migrations/lock_retrier.rb, line 63
def lock_timeout(_attempt)
  raise NotImplementedError
end
with_lock_retries() { || ... } click to toggle source

Executes the block with a retry mechanism that alters the ‘lock_timeout` and sleep time between attempts.

@return [void]

@example

retrier.with_lock_retries do
  add_column(:users, :name, :string)
end
# File lib/online_migrations/lock_retrier.rb, line 85
def with_lock_retries(&block)
  return yield if lock_retries_disabled?

  current_attempt = 0

  begin
    current_attempt += 1

    current_lock_timeout = lock_timeout(current_attempt)
    if current_lock_timeout
      with_lock_timeout(current_lock_timeout.in_milliseconds, &block)
    else
      yield
    end
  # ActiveRecord::LockWaitTimeout can be used for ActiveRecord 5.2+
  rescue ActiveRecord::StatementInvalid => e
    if lock_timeout_error?(e) && current_attempt <= attempts
      current_delay = delay(current_attempt)
      Utils.say("Lock timeout. Retrying in #{current_delay} seconds...")
      sleep(current_delay)
      retry
    end
    raise
  end
end

Private Instance Methods

lock_retries_disabled?() click to toggle source
# File lib/online_migrations/lock_retrier.rb, line 112
def lock_retries_disabled?
  Utils.to_bool(ENV["DISABLE_LOCK_RETRIES"])
end
lock_timeout_error?(error) click to toggle source
# File lib/online_migrations/lock_retrier.rb, line 126
def lock_timeout_error?(error)
  error.message.include?("canceling statement due to lock timeout")
end
with_lock_timeout(value) { || ... } click to toggle source
# File lib/online_migrations/lock_retrier.rb, line 116
def with_lock_timeout(value)
  value = value.ceil.to_i
  prev_value = connection.select_value("SHOW lock_timeout")
  connection.execute("SET lock_timeout TO #{connection.quote("#{value}ms")}")

  yield
ensure
  connection.execute("SET lock_timeout TO #{connection.quote(prev_value)}")
end