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 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