class MysqlFramework::Scripts::LockManager

Public Class Methods

new() click to toggle source
# File lib/mysql_framework/scripts/lock_manager.rb, line 8
def initialize
  @pool = Queue.new
end

Public Instance Methods

fetch_client() click to toggle source

This method is called to retrieve a Redlock client from the pool

# File lib/mysql_framework/scripts/lock_manager.rb, line 65
def fetch_client
  @pool.pop(true)
rescue StandardError
  # By not letting redlock retry we will rely on the retry that happens in this class
  Redlock::Client.new([redis_url], retry_jitter: retry_jitter, retry_count: 1, retry_delay: 0)
end
release_lock(key:, lock:) click to toggle source

This method is called to release a lock

# File lib/mysql_framework/scripts/lock_manager.rb, line 44
def release_lock(key:, lock:)
  return if lock.nil?

  MysqlFramework.logger.info { "[#{self.class}] - Releasing lock: #{key}." }

  with_client { |client| client.unlock(lock) }
end
request_lock(key:, ttl: default_ttl, max_attempts: default_max_retries, retry_delay: default_retry_delay) click to toggle source

This method is called to request a lock (Default 5 minutes)

# File lib/mysql_framework/scripts/lock_manager.rb, line 13
def request_lock(key:, ttl: default_ttl, max_attempts: default_max_retries, retry_delay: default_retry_delay)
  MysqlFramework.logger.info { "[#{self.class}] - Requesting lock: #{key}." }

  lock = false
  count = 0

  loop do
    # request a lock
    lock = with_client { |client| client.lock(key, ttl) }

    # if lock was received break out of the loop
    break if lock

    # lock was not received so increment request count
    count += 1

    MysqlFramework.logger.debug do
      "[#{self.class}] - Key is currently locked, waiting for lock: #{key} | Wait count: #{count}."
    end

    # check if lock requests have exceeded max request attempts
    raise "Resource is already locked. Lock key: #{key}. Max attempt exceeded." if count == max_attempts

    # sleep and try requesting the lock again
    sleep(retry_delay)
  end

  lock
end
with_client() { |client| ... } click to toggle source

This method is called to retrieve a Redlock client from the pool and yield it to a block

# File lib/mysql_framework/scripts/lock_manager.rb, line 73
def with_client
  client = fetch_client
  yield client
ensure
  @pool.push(client)
end
with_lock(key:, ttl: default_ttl, max_attempts: default_max_retries, retry_delay: default_retry_delay) { || ... } click to toggle source

This method is called to request and release a lock around yielding to a user supplied block

# File lib/mysql_framework/scripts/lock_manager.rb, line 53
def with_lock(key:, ttl: default_ttl, max_attempts: default_max_retries, retry_delay: default_retry_delay)
  raise 'Block must be specified.' unless block_given?

  begin
    lock = request_lock(key: key, ttl: ttl, max_attempts: max_attempts, retry_delay: retry_delay)
    yield
  ensure
    release_lock(key: key, lock: lock)
  end
end

Private Instance Methods

default_max_retries() click to toggle source
# File lib/mysql_framework/scripts/lock_manager.rb, line 90
def default_max_retries
  @default_max_retries ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_MAX_ATTEMPTS', 300))
end
default_retry_delay() click to toggle source
# File lib/mysql_framework/scripts/lock_manager.rb, line 94
def default_retry_delay
  @default_retry_delay ||= Float(ENV.fetch('MYSQL_MIGRATION_LOCK_RETRY_DELAY_S', 1.0))
end
default_ttl() click to toggle source
# File lib/mysql_framework/scripts/lock_manager.rb, line 86
def default_ttl
  @default_ttl ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_TTL', 2000))
end
redis_url() click to toggle source
# File lib/mysql_framework/scripts/lock_manager.rb, line 82
def redis_url
  ENV.fetch('REDIS_URL')
end
retry_jitter() click to toggle source
# File lib/mysql_framework/scripts/lock_manager.rb, line 98
def retry_jitter
  @retry_jitter ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_JITTER_MS', 50))
end