module MasterLock
MasterLock
is a system for interprocess locking. Resources can be locked by a string identifier such that only one thread may have the lock at a time. Lock state and owners are stored on a Redis server shared by all processes. Locks are held until either the block of synchronized code completes or the thread that obtained the lock is killed. To prevent the locks from being held indefinitely in the event that the process dies without releasing them, the locks have an expiration time in Redis. While the thread owning the lock is alive, a separate thread will extend the lifetime of the locks so that they do not expire even when the code in the critical section takes a long time to execute.
Constants
- Config
- DEFAULT_ACQUIRE_TIMEOUT
- DEFAULT_EXTEND_INTERVAL
- DEFAULT_KEY_PREFIX
- DEFAULT_SLEEP_TIME
- DEFAULT_TTL
- VERSION
Public Class Methods
@return [Config] MasterLock
configuration settings
# File lib/master_lock.rb, line 126 def config if !defined?(@config) @config = Config.new @config.acquire_timeout = DEFAULT_ACQUIRE_TIMEOUT @config.extend_interval = DEFAULT_EXTEND_INTERVAL @config.hostname = Socket.gethostname @config.logger = Logger.new(STDOUT) @config.logger.progname = name @config.key_prefix = DEFAULT_KEY_PREFIX @config.sleep_time = DEFAULT_SLEEP_TIME @config.ttl = DEFAULT_TTL end @config end
Configure MasterLock
using block syntax. Simply yields {#config} to the block.
@yield [Config] the configuration
# File lib/master_lock.rb, line 145 def configure yield config end
Get the configured logger.
@return [Logger]
# File lib/master_lock.rb, line 121 def logger config.logger end
Starts the background thread to manage and extend currently held locks. The thread remains alive for the lifetime of the process. This must be called before any locks may be acquired.
# File lib/master_lock.rb, line 102 def start @registry = Registry.new Thread.new do loop do @registry.extend_locks sleep(config.sleep_time) end end end
Returns true if the registry has been started, otherwise false @return [Boolean]
# File lib/master_lock.rb, line 114 def started? !@registry.nil? end
Obtain a mutex around a critical section of code. Only one thread on any machine can execute the given block at a time. Returns the result of the block.
@param key [String] the unique identifier for the locked resource @option options [Fixnum] :ttl (60) the length of time in seconds before
the lock expires
@option options [Fixnum] :acquire_timeout (5) the length of time to wait
to acquire the lock before timing out
@option options [Fixnum] :extend_interval (15) the amount of time in
seconds that may pass before extending the lock
@option options [Boolean] :if if this option is falsey, the block will be
executed without obtaining the lock
@option options [Boolean] :unless if this option is truthy, the block will
be executed without obtaining the lock
@raise [UnconfiguredError] if a required configuration variable is unset @raise [NotStartedError] if called before {#start} @raise [LockNotAcquiredError] if the lock cannot be acquired before the
timeout
# File lib/master_lock.rb, line 58 def synchronize(key, options = {}) check_configured raise NotStartedError unless @registry ttl = options[:ttl] || config.ttl acquire_timeout = options[:acquire_timeout] || config.acquire_timeout extend_interval = options[:extend_interval] || config.extend_interval raise ArgumentError, "extend_interval cannot be negative" if extend_interval < 0 raise ArgumentError, "ttl must be greater extend_interval" if ttl <= extend_interval if (options.include?(:if) && !options[:if]) || (options.include?(:unless) && options[:unless]) return yield end lock = RedisLock.new( redis: config.redis, key: key, ttl: ttl, owner: generate_owner ) if !lock.acquire(timeout: acquire_timeout) raise LockNotAcquiredError, key end registration = @registry.register(lock, extend_interval) logger.debug("Acquired lock #{key}") begin yield ensure @registry.unregister(registration) if lock.release logger.debug("Released lock #{key}") else logger.warn("Failed to release lock #{key}") end end end
Private Class Methods
# File lib/master_lock.rb, line 151 def check_configured raise UnconfiguredError, "redis must be configured" unless config.redis end
# File lib/master_lock.rb, line 155 def generate_owner "#{config.hostname}:#{Process.pid}:#{Thread.current.object_id}" end