Class: RedisLock

Inherits:
Object
  • Object
show all
Defined in:
lib/redis_lock.rb

Constant Summary collapse

VERSION =

Gem version

"1.2.0"
DEFAULT_ATTRS =

Original defaults

{
  redis: nil, # Redis instance with defaults
  key: 'RedisLock::default', # Redis key to store the lock
  autorelease: 10.0, # seconds to expire
  retry: true, # false to only try to acquire once
  retry_timeout: 10.0, # max number of seconds to keep doing retries if the lock is not available
  retry_sleep: 0.1 # seconds to sleep before the nex retry
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ RedisLock

Returns a new instance of RedisLock.

Raises:

  • (ArgumentError)


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/redis_lock.rb', line 55

def initialize(opts={})
  # Check if options are valid
  allowed_opts = DEFAULT_ATTRS.keys
  invalid_opts = opts.keys - allowed_opts
  raise ArgumentError.new("Invalid options: #{invalid_opts.inspect}. Please use one of #{allowed_opts.inspect} ") unless invalid_opts.empty?

  # Set attributes from options or defaults
  self.redis = opts[:redis] || @@config.redis || Redis.new
  self.redis = Redis.new(redis) if redis.is_a? Hash # allow to use Redis options instead of a redis instance

  self.key           = opts[:key]           || @@config.key
  self.autorelease   = opts[:autorelease]   || @@config.autorelease
  self.retry         = opts.include?(:retry) ? opts[:retry] : @@config.retry
  self.retry_timeout = opts[:retry_timeout] || @@config.retry_timeout
  self.retry_sleep   = opts[:retry_sleep]   || @@config.retry_sleep
end

Instance Attribute Details

#acquired_tokenObject

if the lock was successfully acquired, this is the token used to identify the lock. False otherwise.



23
24
25
# File 'lib/redis_lock.rb', line 23

def acquired_token
  @acquired_token
end

#last_acquire_retriesObject

info about how many times had to retry to acquire the lock on the last call to acquire. First try counts as 0



24
25
26
# File 'lib/redis_lock.rb', line 24

def last_acquire_retries
  @last_acquire_retries
end

Class Method Details

.acquire(opts = {}, &block) ⇒ Object

Acquire a lock. Use options to override defaults. This method makes sure to release the lock as soon as the block is finalized.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/redis_lock.rb', line 39

def self.acquire(opts={}, &block)
  if block.arity != 1
    raise ArgumentError.new('Expected lock parameter in block. Example: RedisLock.acquire(opts){|lock| do_stuff if lock.acquired? }')
  end
  lock = RedisLock.new(opts)
  if lock.acquire
    begin
      block.call(lock) # lock.acquired? => true
    ensure
      lock.release # Exception => release early
    end
  else
    block.call(lock) # lock.acquired? => false
  end
end

.configure {|@@config| ... } ⇒ Object

Configure defaults

Yields:

  • (@@config)


33
34
35
# File 'lib/redis_lock.rb', line 33

def self.configure
  yield @@config
end

.configure_restore_defaultsObject

Restore original defaults



27
28
29
# File 'lib/redis_lock.rb', line 27

def self.configure_restore_defaults
  @@config = Struct.new(*DEFAULT_ATTRS.keys).new(*DEFAULT_ATTRS.values)
end

Instance Method Details

#acquireObject

Try to acquire the lock. Retrun true on success, false on failure (someone else has the lock)



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/redis_lock.rb', line 74

def acquire
  @first_try_time ||= Time.now
  @token ||= SecureRandom.uuid # token is used to make sure that we own the lock when releasing it
  @retries ||= 0

  # Lock using a redis key, if not exists (NX) with an expiration time (EX).
  # NOTE that the NX and EX options are not supported by REDIS versions older than 2.6.12
  # See lock pattern: http://redis.io/commands/SET
  expire = (autorelease * 1000).to_i # to milliseconds
  if redis.set(key, @token, nx: true, px: expire)
    self.acquired_token = @token # assign acquired_token

  else
    self.acquired_token = nil # clear acquired_token, to make the acquired? method return false

    # Wait and try again if retry option is set and didn't timeout
    if self.retry and (Time.now - @first_try_time) < retry_timeout
      sleep retry_sleep # wait
      @retries += 1
      return acquire # and try again
    end
  end

  self.last_acquire_retries = @retries
  @retries = nil # reset retries
  @first_try_time = nil # reset timestamp

  return self.acquired?
end

#acquired?Boolean

Check if last lock acquisition was successful. Note that it doesn’t track autorelease, if the lock is naturally expired, this value will still be true.

Returns:

  • (Boolean)


130
131
132
# File 'lib/redis_lock.rb', line 130

def acquired?
  !!self.acquired_token # acquired_token is only set on success
end

#releaseObject

Release the lock. Returns a Symbol with the status of the operation:

* :success if properly released
* :already_released if the lock was already released or expired (other process could be using it now)
* :not_acquired if the lock was not acquired (no release action was made because it was not needed)


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/redis_lock.rb', line 109

def release
  if acquired?
    if redis.respond_to? :eval # if eval command is available, run a lua script because is a faster way to remove the key
      script = 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return nil end'
      ret = redis.eval(script, [key], [self.acquired_token])
    else # i.e. MockRedis doesn't have eval
      ret = if redis.get(key) == self.acquired_token then redis.del(key) else nil end
    end
    self.acquired_token = nil # cleanup acquired token
    if ret == nil
      :already_released
    else
      :success
    end
  else
    :not_acquired
  end
end