robust-redis-lock 0.4.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/redis-lock.rb +93 -63
- data/lib/robust-redis-lock/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9fa5c673571893a2695d8a6b4938a0244c1014ba
|
4
|
+
data.tar.gz: da55dd270b522b7a845cf54d999d854efda7352d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 279699f922fe00b4b72428878a044edb0fcb9a411bbb141688ff2d53c6bc1596ce5da6127a41108474064bb2217bcd11081bd2ea12faff97f83ab1c549deb91f
|
7
|
+
data.tar.gz: 2d9e74e71ca98ebcd1f18a274f109b5d51b0bfd1ffebcf207bbb9c20264831041191dfb28d87e205110347ad06e243b43591b8d81f7f1de4ac1084b4fa0bff89
|
data/lib/redis-lock.rb
CHANGED
@@ -1,55 +1,41 @@
|
|
1
1
|
require 'redis'
|
2
|
-
require 'yaml'
|
3
2
|
|
4
3
|
class Redis::Lock
|
4
|
+
NAMESPACE = 'redis:lock'
|
5
|
+
|
5
6
|
require 'robust-redis-lock/script'
|
6
7
|
|
7
8
|
attr_reader :key
|
9
|
+
attr_reader :recovery_data
|
8
10
|
|
9
11
|
class << self
|
10
12
|
attr_accessor :redis
|
11
13
|
attr_accessor :timeout
|
12
14
|
attr_accessor :sleep
|
13
15
|
attr_accessor :expire
|
14
|
-
attr_accessor :namespace
|
15
16
|
attr_accessor :key_group
|
16
|
-
attr_accessor :serializer
|
17
17
|
|
18
18
|
def expired(options={})
|
19
19
|
redis = options[:redis] || self.redis
|
20
|
+
raise "redis cannot be nil" if redis.nil?
|
21
|
+
|
20
22
|
redis.zrangebyscore(key_group_key(options), 0, Time.now.to_i).to_a.map { |key| self.new(key, options) }
|
21
23
|
end
|
22
24
|
|
23
25
|
def key_group_key(options)
|
24
|
-
[
|
25
|
-
end
|
26
|
-
|
27
|
-
def namespace_prefix(options)
|
28
|
-
(options[:namespace] || self.namespace)
|
26
|
+
[NAMESPACE, (options[:key_group] || self.key_group), 'group'].join(':')
|
29
27
|
end
|
30
28
|
end
|
31
29
|
|
32
30
|
self.timeout = 60
|
33
31
|
self.expire = 60
|
34
32
|
self.sleep = 0.1
|
35
|
-
self.namespace = 'redis:lock'
|
36
33
|
self.key_group = 'default'
|
37
|
-
self.serializer = YAML
|
38
|
-
|
39
|
-
def initialize(*args)
|
40
|
-
raise "invalid number of args: expected 1..3, got #{args.length}" if args.length < 1 || args.length > 3
|
41
34
|
|
42
|
-
|
43
|
-
|
35
|
+
def initialize(key, options={})
|
36
|
+
@options = options
|
44
37
|
|
45
|
-
|
46
|
-
@data = args.shift
|
47
|
-
end
|
48
|
-
|
49
|
-
@options = args.shift || {}
|
50
|
-
|
51
|
-
namespace_prefix = self.class.namespace_prefix(@options) unless key.start_with?(self.class.namespace_prefix(@options))
|
52
|
-
@key = [namespace_prefix, key].compact.join(':')
|
38
|
+
@key = key
|
53
39
|
@key_group_key = self.class.key_group_key(@options)
|
54
40
|
|
55
41
|
@redis = @options[:redis] || self.class.redis
|
@@ -58,40 +44,43 @@ class Redis::Lock
|
|
58
44
|
@timeout = @options[:timeout] || self.class.timeout
|
59
45
|
@expire = @options[:expire] || self.class.expire
|
60
46
|
@sleep = @options[:sleep] || self.class.sleep
|
61
|
-
@serializer = @options[:serializer] || self.class.serializer
|
62
47
|
end
|
63
48
|
|
64
|
-
def
|
65
|
-
|
49
|
+
def synchronize(&block)
|
50
|
+
lock
|
51
|
+
begin
|
52
|
+
block.call
|
53
|
+
ensure
|
54
|
+
try_unlock
|
55
|
+
end
|
56
|
+
rescue Recovered
|
57
|
+
end
|
58
|
+
|
59
|
+
def lock(options={})
|
60
|
+
locked = false
|
66
61
|
start_at = now
|
62
|
+
|
67
63
|
while now - start_at < @timeout
|
68
|
-
break if
|
64
|
+
break if locked = try_lock(options)
|
69
65
|
sleep @sleep.to_f
|
70
66
|
end
|
71
67
|
|
72
|
-
|
73
|
-
|
74
|
-
result
|
75
|
-
ensure
|
76
|
-
unlock if block_given?
|
68
|
+
raise Timeout.new(self) unless locked
|
69
|
+
raise Recovered.new(self) if locked == :recovered
|
77
70
|
end
|
78
71
|
|
79
|
-
def
|
80
|
-
|
81
|
-
raw = @redis.hget(key, 'data')
|
82
|
-
@serializer.load(raw) if raw
|
83
|
-
end
|
84
|
-
end
|
72
|
+
def try_lock(options={})
|
73
|
+
raise "recovery_data must be a string" if options[:recovery_data] && !options[:recovery_data].is_a?(String)
|
85
74
|
|
86
|
-
def try_lock
|
87
75
|
# This script loading is not thread safe (touching a class variable), but
|
88
76
|
# that's okay, because the race is harmless.
|
89
77
|
@@lock_script ||= Script.new <<-LUA
|
90
78
|
local key = KEYS[1]
|
91
79
|
local key_group = KEYS[2]
|
92
|
-
local
|
93
|
-
local
|
94
|
-
local
|
80
|
+
local bare_key = ARGV[1]
|
81
|
+
local now = tonumber(ARGV[2])
|
82
|
+
local expires_at = tonumber(ARGV[3])
|
83
|
+
local recovery_data = ARGV[4]
|
95
84
|
local token_key = 'redis:lock:token'
|
96
85
|
|
97
86
|
local prev_expires_at = tonumber(redis.call('hget', key, 'expires_at'))
|
@@ -103,67 +92,78 @@ class Redis::Lock
|
|
103
92
|
|
104
93
|
redis.call('hset', key, 'expires_at', expires_at)
|
105
94
|
redis.call('hset', key, 'token', next_token)
|
106
|
-
redis.call('zadd', key_group, expires_at,
|
107
|
-
|
108
|
-
local prev_data = redis.call('hget', key, 'data')
|
109
|
-
redis.call('hset', key, 'data', data)
|
95
|
+
redis.call('zadd', key_group, expires_at, bare_key)
|
110
96
|
|
111
97
|
if prev_expires_at then
|
112
|
-
return {'recovered', next_token,
|
98
|
+
return {'recovered', next_token, redis.call('hget', key, 'recovery_data')}
|
113
99
|
else
|
100
|
+
redis.call('hset', key, 'recovery_data', recovery_data)
|
114
101
|
return {'acquired', next_token, nil}
|
115
102
|
end
|
116
103
|
LUA
|
117
|
-
result, token,
|
118
|
-
|
119
|
-
|
104
|
+
result, token, recovery_data = @@lock_script.eval(@redis,
|
105
|
+
:keys => [NAMESPACE + @key, @key_group_key],
|
106
|
+
:argv => [@key, now.to_i, now.to_i + @expire, options[:recovery_data]])
|
120
107
|
|
121
108
|
case result
|
122
|
-
when 'locked'
|
109
|
+
when 'locked'
|
110
|
+
false
|
111
|
+
when 'acquired'
|
112
|
+
@token = token
|
113
|
+
true
|
123
114
|
when 'recovered'
|
124
|
-
|
125
|
-
|
126
|
-
|
115
|
+
@token = token
|
116
|
+
@recovery_data = recovery_data
|
117
|
+
:recovered
|
127
118
|
end
|
128
119
|
end
|
129
120
|
|
130
121
|
def unlock
|
122
|
+
raise Redis::Lock::LostLock.new(self) unless try_unlock
|
123
|
+
end
|
124
|
+
|
125
|
+
def try_unlock
|
131
126
|
# Since it's possible that the operations in the critical section took a long time,
|
132
127
|
# we can't just simply release the lock. The unlock method checks if @expire_at
|
133
128
|
# remains the same, and do not release when the lock timestamp was overwritten.
|
134
129
|
@@unlock_script ||= Script.new <<-LUA
|
135
130
|
local key = KEYS[1]
|
136
131
|
local key_group = KEYS[2]
|
137
|
-
local
|
132
|
+
local bare_key = ARGV[1]
|
133
|
+
local token = ARGV[2]
|
138
134
|
|
139
135
|
if redis.call('hget', key, 'token') == token then
|
140
136
|
redis.call('del', key)
|
141
|
-
redis.call('zrem', key_group,
|
137
|
+
redis.call('zrem', key_group, bare_key)
|
142
138
|
return true
|
143
139
|
else
|
144
140
|
return false
|
145
141
|
end
|
146
142
|
LUA
|
147
|
-
|
148
|
-
!!result
|
143
|
+
!!@@unlock_script.eval(@redis, :keys => [NAMESPACE + @key, @key_group_key], :argv => [@key, @token])
|
149
144
|
end
|
150
145
|
|
151
146
|
def extend
|
147
|
+
raise Redis::Lock::LostLock.new(self) unless try_extend
|
148
|
+
end
|
149
|
+
|
150
|
+
def try_extend
|
152
151
|
@@extend_script ||= Script.new <<-LUA
|
153
152
|
local key = KEYS[1]
|
154
153
|
local key_group = KEYS[2]
|
155
|
-
local
|
156
|
-
local
|
154
|
+
local bare_key = ARGV[1]
|
155
|
+
local expires_at = tonumber(ARGV[2])
|
156
|
+
local token = ARGV[3]
|
157
157
|
|
158
158
|
if redis.call('hget', key, 'token') == token then
|
159
159
|
redis.call('hset', key, 'expires_at', expires_at)
|
160
|
-
redis.call('zadd', key_group, expires_at,
|
160
|
+
redis.call('zadd', key_group, expires_at, bare_key)
|
161
161
|
return true
|
162
162
|
else
|
163
163
|
return false
|
164
164
|
end
|
165
165
|
LUA
|
166
|
-
!!@@extend_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i + @expire, @token])
|
166
|
+
!!@@extend_script.eval(@redis, :keys => [NAMESPACE + @key, @key_group_key], :argv => [@key, now.to_i + @expire, @token])
|
167
167
|
end
|
168
168
|
|
169
169
|
def now
|
@@ -171,6 +171,36 @@ class Redis::Lock
|
|
171
171
|
end
|
172
172
|
|
173
173
|
def ==(other)
|
174
|
-
|
174
|
+
@key == other.key
|
175
|
+
end
|
176
|
+
|
177
|
+
def to_s
|
178
|
+
@key
|
179
|
+
end
|
180
|
+
|
181
|
+
class ErrorBase < RuntimeError
|
182
|
+
attr_reader :lock
|
183
|
+
|
184
|
+
def initialize(lock)
|
185
|
+
@lock = lock
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class LostLock < ErrorBase
|
190
|
+
def message
|
191
|
+
"The following lock was lost while trying to modify: #{@lock}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
class Redis::Lock::Recovered < ErrorBase
|
196
|
+
def message
|
197
|
+
"The following lock was recovered: #{@lock}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
class Redis::Lock::Timeout < ErrorBase
|
202
|
+
def message
|
203
|
+
"The following lock timed-out waiting to get aquired: #{@lock}"
|
204
|
+
end
|
175
205
|
end
|
176
206
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: robust-redis-lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kareem Kouddous
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|