robust-redis-lock 0.4.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a87fa0ff80f974931ac04891862c75421405bc71
4
- data.tar.gz: e5eca4757e7d19d6cfd806a276669792ca1f225e
3
+ metadata.gz: 9fa5c673571893a2695d8a6b4938a0244c1014ba
4
+ data.tar.gz: da55dd270b522b7a845cf54d999d854efda7352d
5
5
  SHA512:
6
- metadata.gz: 5d52484eb62093f86949edede7e329ea6b7756e59f66c5b8189959d9f30354831f6fc9cc007aa929d447f1773d087da2b1a1ef108bc04e5f2aad324a217f38e0
7
- data.tar.gz: e4f5c0cfebb70f1ac82ef638a89e80f3aed369bde4e912a06c19780e6d409660cac4acd2d193b8173ca8c5f8ca2184c79d504aaf9f963ce1f19a6e6acc4ed965
6
+ metadata.gz: 279699f922fe00b4b72428878a044edb0fcb9a411bbb141688ff2d53c6bc1596ce5da6127a41108474064bb2217bcd11081bd2ea12faff97f83ab1c549deb91f
7
+ data.tar.gz: 2d9e74e71ca98ebcd1f18a274f109b5d51b0bfd1ffebcf207bbb9c20264831041191dfb28d87e205110347ad06e243b43591b8d81f7f1de4ac1084b4fa0bff89
@@ -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
- [namespace_prefix(options), (options[:key_group] || self.key_group), 'group'].join(':')
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
- key = args.shift
43
- raise "key cannot be nil" if key.nil?
35
+ def initialize(key, options={})
36
+ @options = options
44
37
 
45
- if args.length == 2
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 lock
65
- result = false
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 result = try_lock
64
+ break if locked = try_lock(options)
69
65
  sleep @sleep.to_f
70
66
  end
71
67
 
72
- yield if block_given? && result
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 data
80
- @data ||= begin
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 now = tonumber(ARGV[1])
93
- local expires_at = tonumber(ARGV[2])
94
- local data = ARGV[3]
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, key)
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, prev_data}
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, prev_data = @@lock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i, now.to_i + @expire, @serializer.dump(@data)])
118
-
119
- @token = token if token
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' then return false
109
+ when 'locked'
110
+ false
111
+ when 'acquired'
112
+ @token = token
113
+ true
123
114
  when 'recovered'
124
- prev_data = @serializer.load(prev_data) if prev_data
125
- return prev_data || :recovered
126
- when 'acquired' then return true
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 token = ARGV[1]
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, key)
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
- result = @@unlock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [@token])
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 expires_at = tonumber(ARGV[1])
156
- local token = ARGV[2]
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, key)
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
- self.key == other.key
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
@@ -1,5 +1,5 @@
1
1
  class Redis
2
2
  class Lock
3
- VERSION = '0.4.3'
3
+ VERSION = '1.0.0'
4
4
  end
5
5
  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.3
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: 2014-12-23 00:00:00.000000000 Z
11
+ date: 2015-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis