mario-redis-lock 1.0.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/EXAMPLE_BEER_WAITER.md +46 -0
- data/EXAMPLE_DOG_PILE_EFFECT.md +113 -0
- data/README.md +41 -139
- data/lib/redis_lock.rb +11 -7
- data/mario-redis-lock.gemspec +5 -5
- data/spec/redis_lock_spec.rb +10 -3
- metadata +28 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1f96fb381f15d6699a71780455f6328569d67f3
|
4
|
+
data.tar.gz: 059591adc0ec6438f6d8c9990f16b8bf027aee7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c907b7305b4c0470d50559cae339764b9d5d825ea115c251971604b734b27d11e30bdae9b0a79aabc5629bff40d4dadfbcd7f203bf1c4c14aa903515b722031
|
7
|
+
data.tar.gz: 4b9290554e5061faeb847a6137f891396f1e6e99964e2bffd6376fe61369626f02ca6b722d53eee1090c365f3bdd6b4fc848fee73028ec50c9d0247f30e7a95d
|
@@ -0,0 +1,46 @@
|
|
1
|
+
## Example: Beer Waiter
|
2
|
+
|
3
|
+
In this game, everybody wants a beer but there is only one waiter to attend. Each thread is a thirsty customer, and the Redis lock is the waiter.
|
4
|
+
|
5
|
+
This example can be copy-pasted, just make sure you have redis in localhost (default `Redis.new` instance) and the mario-redis-lock gem installed.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
require 'redis_lock'
|
9
|
+
|
10
|
+
N = 15 # how many people in the bar
|
11
|
+
puts "Starting with #{N} new thirsty customers ..."
|
12
|
+
puts
|
13
|
+
|
14
|
+
RedisLock.configure do |conf|
|
15
|
+
conf.retry_sleep = 1 # call the waiter every second
|
16
|
+
conf.retry_timeout = 10 # wait up to 10 seconds before giving up
|
17
|
+
conf.autorelease = 3 # the waiter will wait a maximun of 3 seconds to be "released" before giving the lock to someone else
|
18
|
+
end
|
19
|
+
|
20
|
+
# Code for a single Thread#i
|
21
|
+
def try_to_get_a_drink(i)
|
22
|
+
name = "Thread##{i}"
|
23
|
+
RedisLock.acquire do |lock|
|
24
|
+
if lock.acquired?
|
25
|
+
puts "<< #{name} gets barman's attention (lock acquired)"
|
26
|
+
sleep 0.2 # time do decide
|
27
|
+
beer = %w(lager pale_ale ipa stout sour)[rand 5]
|
28
|
+
puts ".. #{name} orders a #{beer}"
|
29
|
+
sleep 0.4 # time for the waiter to serve the beer
|
30
|
+
puts ">> #{name} takes the #{beer} and leaves happy :)"
|
31
|
+
puts
|
32
|
+
else
|
33
|
+
puts "!! #{name} is bored of waiting and leaves angry (timeout)"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Start N threads that will be executed in parallel
|
39
|
+
threads = []
|
40
|
+
N.times(){|i| threads << Thread.new(){ try_to_get_a_drink(i) }}
|
41
|
+
threads.each{|thread| thread.join} # do not exit until all threads are done
|
42
|
+
|
43
|
+
puts "DONE"
|
44
|
+
```
|
45
|
+
|
46
|
+
It uses threads for concurrency, but you can also execute this script from different places at the same time in parallel, they share the same lock as far as they use the same Redis instance.
|
@@ -0,0 +1,113 @@
|
|
1
|
+
## Example: Avoid the Dog-Pile effec when invalidating some cached value
|
2
|
+
|
3
|
+
The Dog-Pile effect is a specific case of the [Thundering Herd problem](http://en.wikipedia.org/wiki/Thundering_herd_problem),
|
4
|
+
that happens when a cached value expires and suddenly too many threads try to calculate the new value at the same time.
|
5
|
+
|
6
|
+
Sometimes, the calculation takes expensive resources and it is just fine to do it from just one thread.
|
7
|
+
|
8
|
+
Assume you have a simple cache, a `fetch` function that uses a redis instance.
|
9
|
+
|
10
|
+
Without the lock:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# Retrieve the cached value from the redis key.
|
14
|
+
# If the key is not available, execute the block
|
15
|
+
# and store the new calculated value in the redis key with an expiration time.
|
16
|
+
def fetch(redis, key, expire, &block)
|
17
|
+
redis.get(key) or (
|
18
|
+
val = block.call
|
19
|
+
redis.setex(key, expire, val) if val
|
20
|
+
val
|
21
|
+
)
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
Whith this method, it is easy to optimize slow operations by caching them in Redis.
|
26
|
+
For example, if you want to do a `heavy_database_query`:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require 'redis'
|
30
|
+
redis = Redis.new(url: "redis://:p4ssw0rd@host:6380")
|
31
|
+
expire = 60 # keep the result cached for 1 minute
|
32
|
+
key = 'heavy_query'
|
33
|
+
|
34
|
+
val = fetch redis, key, expire do
|
35
|
+
heavy_database_query # Recalculate if not cached (SLOW)
|
36
|
+
end
|
37
|
+
|
38
|
+
puts val
|
39
|
+
```
|
40
|
+
|
41
|
+
But this fetch could block the database if executed from too many threads, because when the Redis key expires all of them will do the same "heavy_database_query" at the same time.
|
42
|
+
|
43
|
+
To avoid this problem, you can make a `fetch_with_lock` method using a `RedisLock`:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# Retrieve the cached value from the redis key.
|
47
|
+
# If the key is not available, execute the block
|
48
|
+
# and store the new calculated value in the redis key with an expiration time.
|
49
|
+
# The block is executed with a RedisLock to avoid the dog pile effect.
|
50
|
+
# Use the following options:
|
51
|
+
# * :retry_timeout => (default 10) Seconds to stop trying to get the value from redis or the lock.
|
52
|
+
# * :retry_sleep => (default 0.1) Seconds to sleep (block the process) between retries.
|
53
|
+
# * :lock_autorelease => (default same as :retry_timeout) Maximum time in seconds to execute the block. The lock is released after this, assuming that the process failed.
|
54
|
+
# * :lock_key => (default "#{key}_lock") The key used for the lock.
|
55
|
+
def fetch_with_lock(redis, key, expire, opts={}, &block)
|
56
|
+
# Options
|
57
|
+
opts[:retry_timeout] ||= 10
|
58
|
+
opts[:retry_sleep] ||= 0.1
|
59
|
+
opts[:first_try_time] ||= Time.now # used as memory for next retries
|
60
|
+
opts[:lock_key] ||= "#{key}_lock"
|
61
|
+
opts[:lock_autorelease] ||= opts[:retry_timeout]
|
62
|
+
|
63
|
+
# Try to get from redis.
|
64
|
+
val = redis.get(key)
|
65
|
+
return val if val
|
66
|
+
|
67
|
+
# If not in redis, calculate the new value (block.call), but with a RedisLock.
|
68
|
+
RedisLock.acquire({
|
69
|
+
redis: redis,
|
70
|
+
key: opts[:lock_key],
|
71
|
+
autorelease: opts[:lock_autorelease],
|
72
|
+
retry: false,
|
73
|
+
}) do |lock|
|
74
|
+
if lock.acquired?
|
75
|
+
val = block.call # execute block, load/calculate heavy stuff
|
76
|
+
redis.setex(key, expire, val) if val # store in the redis cache
|
77
|
+
end
|
78
|
+
end
|
79
|
+
return val if val
|
80
|
+
|
81
|
+
# If the lock was not available, then someone else was already re-calculating the value.
|
82
|
+
# Just wait a little bit and try again.
|
83
|
+
if (Time.now - opts[:first_try_time]) < opts[:retry_timeout] # unless timed out
|
84
|
+
sleep opts[:retry_sleep]
|
85
|
+
return fetch_with_lock(redis, key, expire, opts, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
# If the lock is still unavailable after the timeout, desist and return nil.
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
```
|
93
|
+
|
94
|
+
Now with this new method, is easy to do the "heavy_database_query", cached in redis and with a lock:
|
95
|
+
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
require 'redis'
|
99
|
+
require 'redis_lock'
|
100
|
+
redis = Redis.new(url: "redis://:p4ssw0rd@host:6380")
|
101
|
+
expire = 60 # keep the result cached for 1 minute
|
102
|
+
key = 'heavy_query'
|
103
|
+
|
104
|
+
val = fetch_with_lock redis, key, expire, retry_timeout: 10, retry_sleep: 1 do
|
105
|
+
heavy_database_query # Recalculate if not cached (SLOW)
|
106
|
+
end
|
107
|
+
|
108
|
+
puts val
|
109
|
+
```
|
110
|
+
|
111
|
+
In this case, the script could be executed from as many threads as we want at the same time, because the "heavy_database_query" is done only once while the other threads wait until the value is cached again or the lock is released.
|
112
|
+
|
113
|
+
|
data/README.md
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
# RedisLock
|
2
2
|
|
3
|
-
Yet another distributed lock
|
3
|
+
Yet another Ruby distributed lock using Redis, with emphasis in transparency.
|
4
4
|
|
5
|
+
Implements the locking algorithm described in the [Redis SET command documentation](http://redis.io/commands/set):
|
5
6
|
|
6
|
-
|
7
|
+
* Acquire lock with `SET {{key}} {{uuid_token}} NX PX {{ms_to_expire}}`
|
8
|
+
* Release lock with `EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return nil end" {{key}} {{uuid_token}}`
|
9
|
+
* Auto release lock if expires
|
7
10
|
|
8
|
-
|
11
|
+
It has the properties:
|
9
12
|
|
10
|
-
|
13
|
+
* Mutual exclusion: At any given moment, only one client can hold a lock
|
14
|
+
* Deadlock free: Eventually it is always possible to acquire a lock, even if the client that locked a resource crashed or gets partitioned
|
15
|
+
* NOT fault tolerant: if the REDIS instance goes down, the lock doesn't work. For a lock wiht liveness guarantee, see [redlock-rb](https://github.com/antirez/redlock-rb), that can use multiple REDIS instances to handle the lock.
|
11
16
|
|
12
17
|
|
13
18
|
## Installation
|
@@ -17,7 +22,7 @@ Requirements:
|
|
17
22
|
* [Redis](http://redis.io/) >= 2.6.12
|
18
23
|
* [redis gem](https://rubygems.org/gems/redis) >= 3.0.5
|
19
24
|
|
20
|
-
The required versions are needed for the new syntax of the SET command
|
25
|
+
The required versions are needed for the new syntax of the SET command (using NX and EX/PX).
|
21
26
|
|
22
27
|
Install from RubyGems:
|
23
28
|
|
@@ -30,14 +35,14 @@ Or include it in your project's `Gemfile` with Bundler:
|
|
30
35
|
|
31
36
|
## Usage
|
32
37
|
|
33
|
-
Acquire the lock to
|
38
|
+
Acquire the lock to `do_exclusive_stuff`:
|
34
39
|
|
35
40
|
```ruby
|
36
|
-
RedisLock.
|
41
|
+
RedisLock.acquire do |lock|
|
37
42
|
if lock.acquired?
|
38
|
-
do_exclusive_stuff # you are the
|
43
|
+
do_exclusive_stuff # you are the only process with the lock, hooray!
|
39
44
|
else
|
40
|
-
oh_well #
|
45
|
+
oh_well # timeout, some other process has the lock and didn't release it before the retry_timeout
|
41
46
|
end
|
42
47
|
end
|
43
48
|
```
|
@@ -49,42 +54,31 @@ Or (equivalent)
|
|
49
54
|
lock = RedisLock.new
|
50
55
|
if lock.acquire
|
51
56
|
begin
|
52
|
-
do_exclusive_stuff # you are the
|
57
|
+
do_exclusive_stuff # you are the only process with the lock, hooray!
|
53
58
|
ensure
|
54
59
|
lock.release
|
55
60
|
end
|
56
61
|
else
|
57
|
-
oh_well #
|
62
|
+
oh_well # timeout, some other process has the lock and didn't release it before the retry_timeout
|
58
63
|
end
|
59
64
|
```
|
60
65
|
|
61
|
-
The class method `RedisLock.
|
62
|
-
|
66
|
+
The class method `RedisLock.acquire(options, &block)` is more concise and releases the lock at the end of the block, even if `do_exclusive_stuff` raises an exception.
|
67
|
+
The second alternative is a little more flexible.
|
68
|
+
|
69
|
+
#### Detailed Usage Examples
|
63
70
|
|
71
|
+
* [Beer Waiter](EXAMPLE_BEER_WAITER.md): Run many threads at the same time, all them try to get a beer in 3 seconds using the same lock. Some will get it, some will timeout.
|
72
|
+
* [Dog Pile Effect](EXAMPLE_DOG_PILE_EFFECT.md): See how to implement a `fetch_with_lock` method, that works like most `Cache.fetch(key, &block)` methods out there (if value is cached in that given key, return the cached value, otherwise run the block), but only executes the block from one of the processes that share that cache, avoiding the case when the cache is invalidated and all processes execute an expensive operation at the same time.
|
64
73
|
|
65
74
|
### Options
|
66
75
|
|
67
76
|
* **redis**: (default `Redis.new`) an instance of Redis, or an options hash to initialize an instance of Redis (see [redis gem](https://rubygems.org/gems/redis)). You can also pass anything that "quaks" like redis, for example an instance of [mock_redis](https://rubygems.org/gems/mock_redis), for testing purposes.
|
68
77
|
* **key**: (default `"RedisLock::default"`) Redis key used for the lock. If you need multiple locks, use a different (unique) key for each lock.
|
69
78
|
* **autorelease**: (default `10.0`) seconds to automatically release (expire) the lock after being acquired. Make sure to give enough time for your "exclusive stuff" to be executed, otherwise other processes could get the lock and start messing with the "exclusive stuff" before this one is done. The autorelease time is important, even when manually doing `lock.realease`, because the process could crash before releasing the lock. Autorelease (expiration time) guarantees that the lock will always be released.
|
70
|
-
* **retry**: (default `true`) boolean to enable/disable consecutive acquire retries in the same `acquire` call. If true, use `retry_timeout` and `retry_sleep` to specify how long and
|
71
|
-
* **retry_timeout**: (default `10.0`)
|
72
|
-
* **retry_sleep**: (default `0.1`) seconds to sleep between retries. For example: `RedisLock.
|
73
|
-
|
74
|
-
Configure the default values with `RedisLock.configure`:
|
75
|
-
|
76
|
-
```ruby
|
77
|
-
RedisLock.configure do |defaults|
|
78
|
-
defaults.redis = Redis.new
|
79
|
-
defaults.key = "RedisLock::default"
|
80
|
-
defaults.autorelease = 10.0
|
81
|
-
defaults.retry = true
|
82
|
-
defaults.retry_timeout = 10.0
|
83
|
-
defaults.retry_sleep = 0.1
|
84
|
-
end
|
85
|
-
```
|
86
|
-
|
87
|
-
A good place to set defaults in a Rails app would be in an initializer `conf/initializers/redis_lock.rb`.
|
79
|
+
* **retry**: (default `true`) boolean to enable/disable consecutive acquire retries in the same `acquire` call. If true, use `retry_timeout` and `retry_sleep` to specify how long and how often should the `acquire` method block the thread (sleep) until able to get the lock.
|
80
|
+
* **retry_timeout**: (default `10.0`) seconds before giving up before the lock is released. Note that the execution thread is put to sleep while waiting. For a non-blocking approach, set `retry` to false.
|
81
|
+
* **retry_sleep**: (default `0.1`) seconds to sleep between retries. For example: `RedisLock.acquire(retry_timeout: 10.0, retry_sleep: 0.1){|lock| ... }` if the lock was acquired by other process and never released, will do almost 100 retries (a rerty every 0.1 seconds, plus a little extra to run the the `SET` command) during 10 seconds, and finally yield with `lock.acquired? == false`.
|
88
82
|
|
89
83
|
Options can be set to other than the defaults when calling `RedisLock.acquire`:
|
90
84
|
|
@@ -99,127 +93,34 @@ end
|
|
99
93
|
Or when creating a new lock instance:
|
100
94
|
|
101
95
|
```ruby
|
102
|
-
lock = RedisLock.new(key: 'exclusive_stuff', retry: false)
|
96
|
+
lock = RedisLock.new(key: 'exclusive_stuff', retry: false, autorelease: 0.1)
|
103
97
|
if lock.acquire
|
104
|
-
|
105
|
-
do_exclusive_stuff
|
106
|
-
ensure
|
107
|
-
lock.release
|
108
|
-
end
|
98
|
+
do_exclusive_stuff_or_not
|
109
99
|
end
|
110
100
|
```
|
111
101
|
|
112
|
-
|
113
|
-
|
114
|
-
If we have a `PhotoBooth` shared resource, we can use a `RedisLock` to ensure it is used only by one thread at a time:
|
102
|
+
You can also configure default values with `RedisLock.configure`:
|
115
103
|
|
116
104
|
```ruby
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
c.autorelease = 60 # assume it never takes more than one minute to make a picture
|
125
|
-
c.retry_timeout = 300 # retry for 5 minutes
|
126
|
-
c.retry_sleep = 1 # retry once every second
|
127
|
-
end
|
128
|
-
|
129
|
-
RedisLock.acquire do |lock|
|
130
|
-
if lock.acquired?
|
131
|
-
PhotoBooth.take_photo
|
132
|
-
else
|
133
|
-
raise "I'm bored of waiting and I'm getting out"
|
134
|
-
end
|
135
|
-
end
|
136
|
-
```
|
137
|
-
|
138
|
-
This script can be executed from many different places at the same time, as far as they have access to the shared PhotoBooth and Redis instances. Only one photo will be taken at a time.
|
139
|
-
Note that the options `autorelease`, `retry_timeout` and `retry_sleep` should be tuned differently depending on the frequency of the operation and the known speed of the `PhotoBooth.take_photo` operation.
|
140
|
-
|
141
|
-
|
142
|
-
### Example: Avoid the Dog-Pile effec when invalidating some cached value
|
143
|
-
|
144
|
-
The Dog-Pile effect is a specific case of the [Thundering Herd problem](http://en.wikipedia.org/wiki/Thundering_herd_problem),
|
145
|
-
that happens when a cached value expires and suddenly too many threads try to calculate the new value at the same time.
|
146
|
-
|
147
|
-
Sometimes, the calculation takes expensive resources and it is just fine to do it from just one thread.
|
148
|
-
|
149
|
-
Assume you have a simple cache, a `fetch` function that uses a redis instance.
|
150
|
-
|
151
|
-
Without the lock:
|
152
|
-
|
153
|
-
```ruby
|
154
|
-
# Retrieve the cached value from the redis key.
|
155
|
-
# If the key is not available, execute the block
|
156
|
-
# and store the new calculated value in the redis key with an expiration time.
|
157
|
-
def fetch(redis, key, expire, &block)
|
158
|
-
val = redis.get(key)
|
159
|
-
if not val
|
160
|
-
val = block.call
|
161
|
-
redis.setex(key, expire, val) unless val.nil? # do not set anything if the value is nil
|
162
|
-
end
|
163
|
-
val
|
164
|
-
end
|
165
|
-
```
|
166
|
-
|
167
|
-
Whith this method, it is easy to optimize slow operations by caching them in Redis.
|
168
|
-
For example, if you want to do a `heavy_database_query`:
|
169
|
-
|
170
|
-
```ruby
|
171
|
-
require 'redis'
|
172
|
-
redis = Redis.new(url: "redis://:p4ssw0rd@host:6380")
|
173
|
-
|
174
|
-
val = fetch redis, 'heavy_query', 10 do
|
175
|
-
heavy_database_query # Recalculate if not cached (SLOW)
|
105
|
+
RedisLock.configure do |defaults|
|
106
|
+
defaults.redis = Redis.new
|
107
|
+
defaults.key = "RedisLock::default"
|
108
|
+
defaults.autorelease = 10.0
|
109
|
+
defaults.retry = true
|
110
|
+
defaults.retry_timeout = 10.0
|
111
|
+
defaults.retry_sleep = 0.1
|
176
112
|
end
|
177
|
-
|
178
|
-
puts val
|
179
113
|
```
|
180
114
|
|
181
|
-
|
182
|
-
|
183
|
-
Avoid this problem with a `RedisLock`:
|
184
|
-
|
185
|
-
```ruby
|
186
|
-
require 'redis'
|
187
|
-
require 'redis_lock'
|
188
|
-
redis = Redis.new(url: "redis://:p4ssw0rd@host:6380")
|
189
|
-
|
190
|
-
RedisLock.configure do |c|
|
191
|
-
c.redis = redis
|
192
|
-
c.key = 'heavy_query_lock'
|
193
|
-
c.autorelease = 20 # assume it never takes more than 20 seconds to do the slow query
|
194
|
-
c.retry = false # try to acquire only once, if the lock is already taken then the new value should be cached again soon
|
195
|
-
end
|
115
|
+
A good place to set defaults in a Rails app would be in an initializer like `conf/initializers/redis_lock.rb`.
|
196
116
|
|
197
|
-
def fetch_with_lock(retries = 10)
|
198
|
-
val = fetch redis, 'heavy_query', 10 do
|
199
|
-
# If we need to recalculate val,
|
200
|
-
# use a lock to make sure that heavy_database_query is only done by one process
|
201
|
-
RedisLock.acquire do |lock|
|
202
|
-
if lock.acquired?
|
203
|
-
heavy_database_query
|
204
|
-
else
|
205
|
-
nil # do not store in cache and return val = nil
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
117
|
|
210
|
-
|
211
|
-
if val.nil? and retries > 0
|
212
|
-
fetch_with_lock(retries - 1)
|
213
|
-
else
|
214
|
-
val
|
215
|
-
end
|
216
|
-
end
|
118
|
+
## Why another Redis lock gem?
|
217
119
|
|
218
|
-
|
219
|
-
puts val
|
220
|
-
```
|
120
|
+
There are other Redis locks for Ruby: [redlock-rb](https://github.com/antirez/redlock-rb), [redis-mutex](https://rubygems.org/gems/redis-mutex), [mlanett-redis-lock](https://rubygems.org/gems/mlanett-redis-lock), [redis-lock](https://rubygems.org/gems/redis-lock), [jashmenn-redis-lock](https://rubygems.org/gems/jashmenn-redis-lock), [ruby_redis_lock](https://rubygems.org/gems/ruby_redis_lock), [robust-redis-lock](https://rubygems.org/gems/robust-redis-lock), [bfg-redis-lock](https://rubygems.org/gems/bfg-redis-lock), etc.
|
221
121
|
|
222
|
-
|
122
|
+
I realized I was not sure how most of them exactly work. What is exactly going on with the lock? When does it expire? How many times needs to retry? Is the thread put to sleep meanwhile?.
|
123
|
+
By the time I learned how to tell if a lock is good or not, I learned enough to write my own, making it simple but explicit, to be used with confidence in my high scale production applications.
|
223
124
|
|
224
125
|
|
225
126
|
## Contributing
|
@@ -232,3 +133,4 @@ In this case, the script could be executed from as many threads as we want at th
|
|
232
133
|
|
233
134
|
Make sure you have installed Redis in localhost:6379. The DB 15 will be used for tests (and flushed after every test).
|
234
135
|
There is a rake task to play with an example: `rake smoke_and_pass`
|
136
|
+
|
data/lib/redis_lock.rb
CHANGED
@@ -4,7 +4,7 @@ require 'securerandom' # SecureRandom (from stdlib)
|
|
4
4
|
class RedisLock
|
5
5
|
|
6
6
|
# Gem version
|
7
|
-
VERSION = "1.0
|
7
|
+
VERSION = "1.2.0"
|
8
8
|
|
9
9
|
# Original defaults
|
10
10
|
DEFAULT_ATTRS = {
|
@@ -37,15 +37,18 @@ class RedisLock
|
|
37
37
|
# Acquire a lock. Use options to override defaults.
|
38
38
|
# This method makes sure to release the lock as soon as the block is finalized.
|
39
39
|
def self.acquire(opts={}, &block)
|
40
|
+
if block.arity != 1
|
41
|
+
raise ArgumentError.new('Expected lock parameter in block. Example: RedisLock.acquire(opts){|lock| do_stuff if lock.acquired? }')
|
42
|
+
end
|
40
43
|
lock = RedisLock.new(opts)
|
41
44
|
if lock.acquire
|
42
45
|
begin
|
43
|
-
block.call(lock)
|
46
|
+
block.call(lock) # lock.acquired? => true
|
44
47
|
ensure
|
45
|
-
lock.release
|
48
|
+
lock.release # Exception => release early
|
46
49
|
end
|
47
50
|
else
|
48
|
-
block.call(lock)
|
51
|
+
block.call(lock) # lock.acquired? => false
|
49
52
|
end
|
50
53
|
end
|
51
54
|
|
@@ -58,11 +61,12 @@ class RedisLock
|
|
58
61
|
# Set attributes from options or defaults
|
59
62
|
self.redis = opts[:redis] || @@config.redis || Redis.new
|
60
63
|
self.redis = Redis.new(redis) if redis.is_a? Hash # allow to use Redis options instead of a redis instance
|
61
|
-
|
62
|
-
self.
|
64
|
+
|
65
|
+
self.key = opts[:key] || @@config.key
|
66
|
+
self.autorelease = opts[:autorelease] || @@config.autorelease
|
63
67
|
self.retry = opts.include?(:retry) ? opts[:retry] : @@config.retry
|
64
68
|
self.retry_timeout = opts[:retry_timeout] || @@config.retry_timeout
|
65
|
-
self.retry_sleep = opts[:retry_sleep]
|
69
|
+
self.retry_sleep = opts[:retry_sleep] || @@config.retry_sleep
|
66
70
|
end
|
67
71
|
|
68
72
|
# Try to acquire the lock.
|
data/mario-redis-lock.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.version = RedisLock::VERSION
|
10
10
|
spec.authors = ["Mario Izquierdo"]
|
11
11
|
spec.email = ["tomario@gmail.com"]
|
12
|
-
spec.summary = %q{Yet another distributed lock
|
13
|
-
spec.description = %q{Yet another distributed lock
|
12
|
+
spec.summary = %q{Yet another Ruby distributed lock using Redis, with emphasis in transparency.}
|
13
|
+
spec.description = %q{Yet another Ruby distributed lock using Redis, with emphasis in transparency. Requires Redis >= 2.6.12, because it uses the new syntax for SET to easily implement the robust algorithm described in the SET command documentation (http://redis.io/commands/set).}
|
14
14
|
spec.homepage = "https://github.com/marioizquierdo/mario-redis-lock"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
@@ -19,8 +19,8 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
|
-
spec.add_runtime_dependency 'redis', '>= 3.0.5' # Needed support for SET with EX, PX, NX, XX options: https://github.com/redis/redis-rb/pull/343
|
22
|
+
spec.add_runtime_dependency 'redis', '~> 3', '>= 3.0.5' # Needed support for SET with EX, PX, NX, XX options: https://github.com/redis/redis-rb/pull/343
|
23
23
|
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.add_development_dependency
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10'
|
26
26
|
end
|
data/spec/redis_lock_spec.rb
CHANGED
@@ -28,7 +28,7 @@ describe RedisLock do
|
|
28
28
|
end
|
29
29
|
|
30
30
|
|
31
|
-
describe "configure" do
|
31
|
+
describe ".configure" do
|
32
32
|
it "changes the defaults" do
|
33
33
|
RedisLock.configure do |conf|
|
34
34
|
conf.key = "mykey"
|
@@ -53,10 +53,12 @@ describe RedisLock do
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
describe "acquire" do
|
56
|
+
describe ".acquire" do
|
57
57
|
describe "with bad redis connection" do
|
58
58
|
it "raises a Redis::CannotConnectError" do
|
59
|
-
proc {
|
59
|
+
proc {
|
60
|
+
RedisLock.acquire(redis: {url: "redis://localhost:1111/15"}){|lock| }
|
61
|
+
}.must_raise Redis::CannotConnectError
|
60
62
|
end
|
61
63
|
end
|
62
64
|
|
@@ -82,6 +84,11 @@ describe RedisLock do
|
|
82
84
|
lock.autorelease.must_equal 5555
|
83
85
|
end
|
84
86
|
end
|
87
|
+
it "does not allow to pass a block with no |lock|" do
|
88
|
+
proc {
|
89
|
+
RedisLock.acquire(){ puts "I should not have been printed" }
|
90
|
+
}.must_raise ArgumentError, "You should use lock"
|
91
|
+
end
|
85
92
|
end
|
86
93
|
|
87
94
|
describe "initialize" do
|
metadata
CHANGED
@@ -1,68 +1,75 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mario-redis-lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mario Izquierdo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-02-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
- - ">="
|
18
21
|
- !ruby/object:Gem::Version
|
19
22
|
version: 3.0.5
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- -
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3'
|
30
|
+
- - ">="
|
25
31
|
- !ruby/object:Gem::Version
|
26
32
|
version: 3.0.5
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: bundler
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
30
36
|
requirements:
|
31
|
-
- - ~>
|
37
|
+
- - "~>"
|
32
38
|
- !ruby/object:Gem::Version
|
33
|
-
version: '1
|
39
|
+
version: '1'
|
34
40
|
type: :development
|
35
41
|
prerelease: false
|
36
42
|
version_requirements: !ruby/object:Gem::Requirement
|
37
43
|
requirements:
|
38
|
-
- - ~>
|
44
|
+
- - "~>"
|
39
45
|
- !ruby/object:Gem::Version
|
40
|
-
version: '1
|
46
|
+
version: '1'
|
41
47
|
- !ruby/object:Gem::Dependency
|
42
48
|
name: rake
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
44
50
|
requirements:
|
45
|
-
- -
|
51
|
+
- - "~>"
|
46
52
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
53
|
+
version: '10'
|
48
54
|
type: :development
|
49
55
|
prerelease: false
|
50
56
|
version_requirements: !ruby/object:Gem::Requirement
|
51
57
|
requirements:
|
52
|
-
- -
|
58
|
+
- - "~>"
|
53
59
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
55
|
-
description: Yet another distributed lock
|
56
|
-
|
57
|
-
|
58
|
-
(http://redis.io/commands/set).
|
60
|
+
version: '10'
|
61
|
+
description: Yet another Ruby distributed lock using Redis, with emphasis in transparency.
|
62
|
+
Requires Redis >= 2.6.12, because it uses the new syntax for SET to easily implement
|
63
|
+
the robust algorithm described in the SET command documentation (http://redis.io/commands/set).
|
59
64
|
email:
|
60
65
|
- tomario@gmail.com
|
61
66
|
executables: []
|
62
67
|
extensions: []
|
63
68
|
extra_rdoc_files: []
|
64
69
|
files:
|
65
|
-
- .gitignore
|
70
|
+
- ".gitignore"
|
71
|
+
- EXAMPLE_BEER_WAITER.md
|
72
|
+
- EXAMPLE_DOG_PILE_EFFECT.md
|
66
73
|
- Gemfile
|
67
74
|
- LICENSE.txt
|
68
75
|
- README.md
|
@@ -80,19 +87,19 @@ require_paths:
|
|
80
87
|
- lib
|
81
88
|
required_ruby_version: !ruby/object:Gem::Requirement
|
82
89
|
requirements:
|
83
|
-
- -
|
90
|
+
- - ">="
|
84
91
|
- !ruby/object:Gem::Version
|
85
92
|
version: '0'
|
86
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
94
|
requirements:
|
88
|
-
- -
|
95
|
+
- - ">="
|
89
96
|
- !ruby/object:Gem::Version
|
90
97
|
version: '0'
|
91
98
|
requirements: []
|
92
99
|
rubyforge_project:
|
93
|
-
rubygems_version: 2.
|
100
|
+
rubygems_version: 2.4.5
|
94
101
|
signing_key:
|
95
102
|
specification_version: 4
|
96
|
-
summary: Yet another distributed lock
|
103
|
+
summary: Yet another Ruby distributed lock using Redis, with emphasis in transparency.
|
97
104
|
test_files:
|
98
105
|
- spec/redis_lock_spec.rb
|