Class: Redis::Lock

Inherits:
Object
  • Object
show all
Defined in:
lib/redis-lock.rb,
lib/redis-lock/version.rb

Defined Under Namespace

Classes: LockError

Constant Summary collapse

VERSION =
"0.0.1"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(redis, lock_name, options = {}) ⇒ Lock

Returns a new instance of Lock.



21
22
23
24
25
26
27
28
29
30
# File 'lib/redis-lock.rb', line 21

def initialize(redis, lock_name, options = {})
  @redis = redis
  @lockname = "lock:#{lock_name}"
  @acquire_timeout = options[:acquire_timeout] || 5
  @lock_duration = options[:lock_duration] || 10
  @logger = options[:logger]
  
  # generate a unique UUID for this lock
  @id = SecureRandom.uuid
end

Instance Attribute Details

#acquire_timeoutObject (readonly)

Returns the value of attribute acquire_timeout.



15
16
17
# File 'lib/redis-lock.rb', line 15

def acquire_timeout
  @acquire_timeout
end

#before_delete_callbackObject

Returns the value of attribute before_delete_callback.



18
19
20
# File 'lib/redis-lock.rb', line 18

def before_delete_callback
  @before_delete_callback
end

#before_extend_callbackObject

Returns the value of attribute before_extend_callback.



19
20
21
# File 'lib/redis-lock.rb', line 19

def before_extend_callback
  @before_extend_callback
end

#idObject (readonly)

Returns the value of attribute id.



13
14
15
# File 'lib/redis-lock.rb', line 13

def id
  @id
end

#lock_durationObject (readonly)

Returns the value of attribute lock_duration.



16
17
18
# File 'lib/redis-lock.rb', line 16

def lock_duration
  @lock_duration
end

#locknameObject (readonly)

Returns the value of attribute lockname.



14
15
16
# File 'lib/redis-lock.rb', line 14

def lockname
  @lockname
end

#loggerObject (readonly)

Returns the value of attribute logger.



17
18
19
# File 'lib/redis-lock.rb', line 17

def logger
  @logger
end

#redisObject (readonly)

Returns the value of attribute redis.



12
13
14
# File 'lib/redis-lock.rb', line 12

def redis
  @redis
end

Instance Method Details

#acquire_lockObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/redis-lock.rb', line 51

def acquire_lock
  try_until = Time.now + acquire_timeout
  
  # loop until now + timeout trying to get the lock
  while Time.now < try_until
    log :debug, "attempting to acquire lock #{lockname}"

    # try and obtain the lock
    if redis.setnx(lockname, id)
      log :info, "lock #{lockname} acquired for #{id}"
      # lock was obtained, so add an expiration
      add_expiration
      return true
    elsif missing_expiration?
      # if no expiration, client that obtained lock likely crashed - add an expiration
      # and wait
      log :debug, "expiration missing on lock #{lockname}"
      add_expiration
    end
    
    # didn't get the lock, sleep briefly and try again
    sleep(0.001)
  end
  
  # was never able to get the lock - give up
  return false
end

#add_expirationObject



150
151
152
153
# File 'lib/redis-lock.rb', line 150

def add_expiration()
  log :debug, "adding expiration of #{lock_duration} seconds to #{lockname}"
  redis.expire(lockname, lock_duration)
end

#extend_lock(extend_by = 10) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/redis-lock.rb', line 79

def extend_lock(extend_by = 10)
  begin
    with_watch do
      if lock_owner?
        log :debug, "we are the lock owner - extending lock by #{extend_by} seconds"
        
        # check if we want to do a callback
        if before_extend_callback
          log :debug, "calling callback"
          before_extend_callback.call(redis)
        end
        
        redis.multi do |multi|
          multi.expire lockname, extend_by
        end
        
        # we extended the lock, return the lock
        return self
      end

      log :debug, "we aren't the lock owner - raising LockError"

      # we aren't the lock owner anymore - raise LockError
      raise LockError.new("unable to extend #{lockname} - no longer the lock owner")
    end
  rescue LockError => e
    raise e
  rescue StandardError => e
    log :warn, "#{lockname} changed while attempting to release key - retrying"
    # try extending the lock again, just in case
    extend_lock extend_by
  end
end

#lock(&block) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/redis-lock.rb', line 32

def lock(&block)
  acquire_lock or raise LockError.new(lockname)
  
  if block
    begin
      block.call(self)
    ensure
      release_lock
    end
  end
  
  self
end

#lock_owner?Boolean

Returns:

  • (Boolean)


155
156
157
158
# File 'lib/redis-lock.rb', line 155

def lock_owner?
  log :debug, "our id: #{id} - lock owner: #{redis.get(lockname)}"
  redis.get(lockname) == id
end

#locked?Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/redis-lock.rb', line 142

def locked?
  lock_owner?
end

#log(level, message) ⇒ Object



180
181
182
183
184
# File 'lib/redis-lock.rb', line 180

def log(level, message)
  if logger
    logger.send(level) { message }
  end
end

#missing_expiration?Boolean

Returns:

  • (Boolean)


146
147
148
# File 'lib/redis-lock.rb', line 146

def missing_expiration?
  redis.ttl(lockname) == -1
end

#release_lockObject



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/redis-lock.rb', line 113

def release_lock
  # we are going to watch the lock key while attempting to remove it, so we can
  # retry removing the lock if the lock is changed while we are removing it.
  release_with_watch do

    log :debug, "releasing #{lockname}..."

    # make sure we still own the lock
    if lock_owner?
      log :debug, "we are the lock owner"
      
      # check if we want to do a callback
      if before_delete_callback
        log :debug, "calling callback"
        before_delete_callback.call(redis)
      end

      redis.multi do |multi|
        multi.del lockname
      end
      return true
    end
    
    # we weren't the owner of the lock anymore - just return
    return false

  end
end

#release_with_watch(&block) ⇒ Object



160
161
162
163
164
165
166
167
168
169
# File 'lib/redis-lock.rb', line 160

def release_with_watch(&block)
  with_watch do
    begin
      block.call
    rescue => e
      log :warn, "#{lockname} changed while attempting to release key - retrying"
      release_with_watch &block
    end
  end
end

#unlockObject



46
47
48
49
# File 'lib/redis-lock.rb', line 46

def unlock
  release_lock
  self
end

#with_watch(&block) ⇒ Object



171
172
173
174
175
176
177
178
# File 'lib/redis-lock.rb', line 171

def with_watch(&block)
  redis.watch lockname
  begin
    block.call
  ensure
    redis.unwatch
  end
end