Class: Redis::Lock

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

Defined Under Namespace

Classes: Script

Constant Summary collapse

VERSION =
'0.4.0'

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Lock

Returns a new instance of Lock.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/redis-lock.rb', line 38

def initialize(*args)
  raise "invalid number of args: expected 1..3, got #{args.length}" if args.length < 1 || args.length > 3

  key = args.shift
  raise "key cannot be nil" if key.nil?

  if args.length == 2
    @data = args.shift
  end

  @options = args.shift || {}

  namespace_prefix = self.class.namespace_prefix(@options) unless key.start_with?(self.class.namespace_prefix(@options))
  @key = [namespace_prefix, key].compact.join(':')
  @key_group_key = self.class.key_group_key(@options)

  @redis    = @options[:redis] || self.class.redis
  raise "redis cannot be nil" if @redis.nil?

  @timeout    = @options[:timeout]    || self.class.timeout
  @expire     = @options[:expire]     || self.class.expire
  @sleep      = @options[:sleep]      || self.class.sleep
  @serializer = @options[:serializer] || self.class.serializer
end

Class Attribute Details

.expireObject

Returns the value of attribute expire.



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

def expire
  @expire
end

.key_groupObject

Returns the value of attribute key_group.



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

def key_group
  @key_group
end

.namespaceObject

Returns the value of attribute namespace.



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

def namespace
  @namespace
end

.redisObject

Returns the value of attribute redis.



10
11
12
# File 'lib/redis-lock.rb', line 10

def redis
  @redis
end

.serializerObject

Returns the value of attribute serializer.



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

def serializer
  @serializer
end

.sleepObject

Returns the value of attribute sleep.



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

def sleep
  @sleep
end

.timeoutObject

Returns the value of attribute timeout.



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

def timeout
  @timeout
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key.



7
8
9
# File 'lib/redis-lock.rb', line 7

def key
  @key
end

Class Method Details

.expired(options = {}) ⇒ Object



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

def expired(options={})
  self.redis.zrangebyscore(key_group_key(options), 0, Time.now.to_i).map { |key| self.new(key, options) }
end

.key_group_key(options) ⇒ Object



22
23
24
# File 'lib/redis-lock.rb', line 22

def key_group_key(options)
  [namespace_prefix(options), (options[:key_group] || self.key_group), 'group'].join(':')
end

.namespace_prefix(options) ⇒ Object



26
27
28
# File 'lib/redis-lock.rb', line 26

def namespace_prefix(options)
  (options[:namespace] || self.namespace)
end

Instance Method Details

#==(other) ⇒ Object



165
166
167
# File 'lib/redis-lock.rb', line 165

def ==(other)
  self.key == other.key
end

#dataObject



78
79
80
# File 'lib/redis-lock.rb', line 78

def data
  @data ||= @serializer.load(@redis.hget(key, 'data'))
end

#extendObject



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/redis-lock.rb', line 143

def extend
  @@extend_script ||= Script.new "      local key = KEYS[1]\n      local key_group = KEYS[2]\n      local expires_at = tonumber(ARGV[1])\n      local token = ARGV[2]\n\n      if redis.call('hget', key, 'token') == token then\n        redis.call('hset', key, 'expires_at', expires_at)\n        redis.call('zadd', key_group, expires_at, key)\n        return true\n      else\n        return false\n      end\n  LUA\n  !!@@extend_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i + @expire, @token])\nend\n"

#lockObject



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/redis-lock.rb', line 63

def lock
  result = false
  start_at = now
  while now - start_at < @timeout
    break if result = try_lock
    sleep @sleep.to_f
  end

  yield if block_given? && result

  result
ensure
  unlock if block_given?
end

#nowObject



161
162
163
# File 'lib/redis-lock.rb', line 161

def now
  Time.now
end

#try_lockObject



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
112
113
114
115
116
117
118
119
120
# File 'lib/redis-lock.rb', line 82

def try_lock
  # This script loading is not thread safe (touching a class variable), but
  # that's okay, because the race is harmless.
  @@lock_script ||= Script.new "      local key = KEYS[1]\n      local key_group = KEYS[2]\n      local now = tonumber(ARGV[1])\n      local expires_at = tonumber(ARGV[2])\n      local data = ARGV[3]\n      local token_key = 'redis:lock:token'\n\n      local prev_expires_at = tonumber(redis.call('hget', key, 'expires_at'))\n      if prev_expires_at and prev_expires_at > now then\n        return {'locked', nil}\n      end\n\n      local next_token = redis.call('incr', token_key)\n\n      redis.call('hset', key, 'expires_at', expires_at)\n      redis.call('hset', key, 'token', next_token)\n      redis.call('hset', key, 'data', data)\n      redis.call('zadd', key_group, expires_at, key)\n\n      if prev_expires_at then\n        return {'recovered', next_token}\n      else\n        return {'acquired', next_token}\n      end\n  LUA\n  result, token = @@lock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i, now.to_i + @expire, @serializer.dump(@data)])\n\n  @token = token if token\n\n  case result\n  when 'locked'    then return false\n  when 'recovered' then return :recovered\n  when 'acquired'  then return true\n  end\nend\n"

#unlockObject



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/redis-lock.rb', line 122

def unlock
  # Since it's possible that the operations in the critical section took a long time,
  # we can't just simply release the lock. The unlock method checks if @expire_at
  # remains the same, and do not release when the lock timestamp was overwritten.
  @@unlock_script ||= Script.new "      local key = KEYS[1]\n      local key_group = KEYS[2]\n      local token = ARGV[1]\n\n      if redis.call('hget', key, 'token') == token then\n        redis.call('del', key)\n        redis.call('zrem', key_group, key)\n        return true\n      else\n        return false\n      end\n  LUA\n  result = @@unlock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [@token])\n  !!result\nend\n"