Module: MasterLock

Defined in:
lib/master_lock.rb,
lib/master_lock/version.rb,
lib/master_lock/registry.rb,
lib/master_lock/redis_lock.rb,
lib/master_lock/redis_scripts.rb

Overview

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.

Defined Under Namespace

Modules: RedisScripts Classes: Config, LockNotAcquiredError, NotStartedError, RedisLock, Registry, UnconfiguredError

Constant Summary collapse

DEFAULT_ACQUIRE_TIMEOUT =
5
DEFAULT_EXTEND_INTERVAL =
15
DEFAULT_KEY_PREFIX =
"masterlock".freeze
DEFAULT_SLEEP_TIME =
5
DEFAULT_TTL =
60
VERSION =
"0.11.0"

Class Method Summary collapse

Class Method Details

.configConfig

Returns MasterLock configuration settings.

Returns:

  • (Config)

    MasterLock configuration settings



126
127
128
129
130
131
132
133
134
135
136
137
138
139
# 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 {|Config| ... } ⇒ Object

Configure MasterLock using block syntax. Simply yields #config to the block.

Yields:

  • (Config)

    the configuration



145
146
147
# File 'lib/master_lock.rb', line 145

def configure
  yield config
end

.loggerLogger

Get the configured logger.

Returns:

  • (Logger)


121
122
123
# File 'lib/master_lock.rb', line 121

def logger
  config.logger
end

.startObject

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.



102
103
104
105
106
107
108
109
110
# 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

.started?Boolean

Returns true if the registry has been started, otherwise false

Returns:

  • (Boolean)


114
115
116
# File 'lib/master_lock.rb', line 114

def started?
  !@registry.nil?
end

.synchronize(key, options = {}) ⇒ Object

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.

Parameters:

  • key (String)

    the unique identifier for the locked resource

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :ttl (Fixnum) — default: 60

    the length of time in seconds before the lock expires

  • :acquire_timeout (Fixnum) — default: 5

    the length of time to wait to acquire the lock before timing out

  • :extend_interval (Fixnum) — default: 15

    the amount of time in seconds that may pass before extending the lock

  • :if (Boolean)

    if this option is falsey, the block will be executed without obtaining the lock

  • :unless (Boolean)

    if this option is truthy, the block will be executed without obtaining the lock

Raises:



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# 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