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
Database connection on which retries are run
Public Instance Methods
Returns the number of retrying attempts
# File lib/online_migrations/lock_retrier.rb, line 55 def attempts raise NotImplementedError end
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
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
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
# File lib/online_migrations/lock_retrier.rb, line 112 def lock_retries_disabled? Utils.to_bool(ENV["DISABLE_LOCK_RETRIES"]) end
# File lib/online_migrations/lock_retrier.rb, line 126 def lock_timeout_error?(error) error.message.include?("canceling statement due to lock timeout") end
# 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