redis-semaphore 0.2.3.1 → 0.2.4
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 +8 -8
- data/README.md +28 -9
- data/lib/redis/semaphore.rb +34 -12
- data/spec/semaphore_spec.rb +42 -0
- data/spec/spec_helper.rb +2 -7
- metadata +58 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MjdjZjZhZWZiNmZiYTY1YWJkMWQ0ZDUyYmJjZGY2NGViMzQzZDhmMA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MjI5YzlmMzBhOWE2ZmUwZTQyNjQ0Y2JjM2ZkMzZhOWEzMTc0YWZkOA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YzZjOTA0ZWQ2OWUzMzk2YTYyODA1ZWIzYjBiZmU1MTc5Yjg4MTk0MDZjYWYz
|
10
|
+
YjZiZTZlZTBmYzQ0MWFlNjFlNzFiODVjOTc4YjAyOWRiODUwYWY4MjNiNmYy
|
11
|
+
ZDAyZDEwOTZjZjk3ODVmMWIyNmJkNWZiYmEyMzM4ZDNjYmU2MTI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MTdhN2RjYTRjMDIxMGJhNTM4YWM0YTViYzNjM2FjYmJmMjBmYjgwZGZjMDE0
|
14
|
+
MGNiZmM3YjI3OTJiNmU5OTAyZTZhNWVmOGFlNzQyZjMxNTYwZmIzMDY2NGU3
|
15
|
+
NTA4NTBkMGNiZjRhNmQzZjEyNjM4ZjMwNzFjYTI4YzgxZWFiYzg=
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
[](https://codeclimate.com/github/dv/redis-semaphore)
|
2
|
+
[](https://travis-ci.org/dv/redis-semaphore)
|
2
3
|
|
3
4
|
redis-semaphore
|
4
5
|
===============
|
@@ -7,7 +8,7 @@ Implements a mutex and semaphore using Redis and the neat BLPOP command.
|
|
7
8
|
|
8
9
|
The mutex and semaphore is blocking, not polling, and has a fair queue serving processes on a first-come, first-serve basis. It can also have an optional timeout after which a lock is unlocked automatically, to protect against dead clients.
|
9
10
|
|
10
|
-
For more info see [Wikipedia](http://en.wikipedia.org/wiki/Semaphore_(programming
|
11
|
+
For more info see [Wikipedia](http://en.wikipedia.org/wiki/Semaphore_(programming)).
|
11
12
|
|
12
13
|
Usage
|
13
14
|
-----
|
@@ -15,7 +16,7 @@ Usage
|
|
15
16
|
Create a mutex:
|
16
17
|
|
17
18
|
```ruby
|
18
|
-
s = Redis::Semaphore.new(:semaphore_name, :
|
19
|
+
s = Redis::Semaphore.new(:semaphore_name, :host => "localhost")
|
19
20
|
s.lock do
|
20
21
|
# We're now in a mutex protected area
|
21
22
|
# No matter how many processes are running this program,
|
@@ -29,7 +30,7 @@ While our application is inside the code block given to ```s.lock```, other call
|
|
29
30
|
You can also allow a set number of processes inside the semaphore-protected block, in case you have a well-defined number of resources available:
|
30
31
|
|
31
32
|
```ruby
|
32
|
-
s = Redis::Semaphore.new(:semaphore_name, :resources => 5, :
|
33
|
+
s = Redis::Semaphore.new(:semaphore_name, :resources => 5, :host => "localhost")
|
33
34
|
s.lock do
|
34
35
|
# Up to five processes at a time will be able to get inside this code
|
35
36
|
# block simultaneously.
|
@@ -40,7 +41,7 @@ end
|
|
40
41
|
You're not obligated to use code blocks, linear calls work just fine:
|
41
42
|
|
42
43
|
```ruby
|
43
|
-
s = Redis::Semaphore.new(:semaphore_name, :
|
44
|
+
s = Redis::Semaphore.new(:semaphore_name, :host => "localhost")
|
44
45
|
s.lock
|
45
46
|
work
|
46
47
|
s.unlock # Don't forget this, or the mutex will stay locked!
|
@@ -75,7 +76,7 @@ sem.available_count # also returns 1
|
|
75
76
|
In the constructor you can pass in any arguments that you would pass to a regular Redis constructor. You can even pass in your custom Redis client:
|
76
77
|
|
77
78
|
```ruby
|
78
|
-
r = Redis.new(:
|
79
|
+
r = Redis.new(:host => "localhost", :db => 222)
|
79
80
|
s = Redis::Semaphore.new(:another_name, :redis => r)
|
80
81
|
#...
|
81
82
|
```
|
@@ -107,10 +108,10 @@ s = Redis::Semaphore.new(:stale_semaphore, :redis = r, :stale_client_timeout =>
|
|
107
108
|
Or you could start a different thread or program that frequently checks for stale locks. This has the advantage of unblocking blocking calls to Semaphore#lock as well:
|
108
109
|
|
109
110
|
```ruby
|
110
|
-
normal_sem = Redis::Semaphore.new(:semaphore, :
|
111
|
+
normal_sem = Redis::Semaphore.new(:semaphore, :host => "localhost")
|
111
112
|
|
112
113
|
Thread.new do
|
113
|
-
watchdog = Redis::Semaphore.new(:semaphore, :
|
114
|
+
watchdog = Redis::Semaphore.new(:semaphore, :host => "localhost", :stale_client_timeout => 5)
|
114
115
|
|
115
116
|
while(true) do
|
116
117
|
watchdog.release_stale_locks!
|
@@ -175,6 +176,16 @@ s = Redis::Semaphore.new(:local_semaphore, :redis = r, :stale_client_timeout =>
|
|
175
176
|
Redis servers earlier than version 2.6 don't support the TIME command. In that case we fall back to using the local time automatically.
|
176
177
|
|
177
178
|
|
179
|
+
### Expiration
|
180
|
+
|
181
|
+
```redis-semaphore``` supports an expiration option, which will call the **EXPIRE** Redis command on all related keys (except for `grabbed_keys`), to make sure that after a while all evidence of the semaphore will disappear and your Redis server will not be cluttered with unused keys. Pass in the expiration timeout in seconds:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
s = Redis::Semaphore.new(:local_semaphore, :redis = r, :expiration => 100)
|
185
|
+
```
|
186
|
+
|
187
|
+
This option should only be used if you know what you're doing. If you chose a wrong expiration timeout then the semaphore might disappear in the middle of a critical section. For most situations just using the `delete!` command should suffice to remove all semaphore keys from the server after you're done using the semaphore.
|
188
|
+
|
178
189
|
Installation
|
179
190
|
------------
|
180
191
|
|
@@ -189,6 +200,10 @@ Testing
|
|
189
200
|
Changelog
|
190
201
|
---------
|
191
202
|
|
203
|
+
###0.2.4 January 11, 2015
|
204
|
+
- Fix bug with TIME and redis-namespace (thanks sos4nt!).
|
205
|
+
- Add expiration option (thanks jcalvert!).
|
206
|
+
|
192
207
|
###0.2.3 September 7, 2014
|
193
208
|
- Block-based locking return the value of the block (thanks frobcode!).
|
194
209
|
|
@@ -237,7 +252,7 @@ Author
|
|
237
252
|
Contributors
|
238
253
|
------------
|
239
254
|
|
240
|
-
Thanks to these awesome
|
255
|
+
Thanks to these awesome people for their contributions:
|
241
256
|
|
242
257
|
- [Rimas Silkaitis](https://github.com/neovintage)
|
243
258
|
- [Tim Galeckas](https://github.com/timgaleckas)
|
@@ -248,3 +263,7 @@ Thanks to these awesome peeps for their contributions:
|
|
248
263
|
- [presskey](https://github.com/presskey)
|
249
264
|
- [Stephen Bussey](https://github.com/sb8244)
|
250
265
|
- [frobcode](https://github.com/frobcode)
|
266
|
+
- [Petteri Räty](https://github.com/betelgeuse)
|
267
|
+
- [Stefan Schüßler](https://github.com/sos4nt)
|
268
|
+
- [Jonathan Calvert](https://github.com/jcalvert)
|
269
|
+
|
data/lib/redis/semaphore.rb
CHANGED
@@ -2,19 +2,21 @@ require 'redis'
|
|
2
2
|
|
3
3
|
class Redis
|
4
4
|
class Semaphore
|
5
|
+
EXISTS_TOKEN = "1"
|
5
6
|
API_VERSION = "1"
|
6
7
|
|
7
|
-
#stale_client_timeout is the threshold of time before we assume
|
8
|
-
#that something has gone terribly wrong with a client and we
|
9
|
-
#invalidate it's lock.
|
8
|
+
# stale_client_timeout is the threshold of time before we assume
|
9
|
+
# that something has gone terribly wrong with a client and we
|
10
|
+
# invalidate it's lock.
|
10
11
|
# Default is nil for which we don't check for stale clients
|
11
12
|
# Redis::Semaphore.new(:my_semaphore, :stale_client_timeout => 30, :redis => myRedis)
|
12
13
|
# Redis::Semaphore.new(:my_semaphore, :redis => myRedis)
|
13
14
|
# Redis::Semaphore.new(:my_semaphore, :resources => 1, :redis => myRedis)
|
14
|
-
# Redis::Semaphore.new(:my_semaphore, :
|
15
|
+
# Redis::Semaphore.new(:my_semaphore, :host => "", :port => "")
|
15
16
|
# Redis::Semaphore.new(:my_semaphore, :path => "bla")
|
16
17
|
def initialize(name, opts = {})
|
17
18
|
@name = name
|
19
|
+
@expiration = opts.delete(:expiration)
|
18
20
|
@resource_count = opts.delete(:resources) || 1
|
19
21
|
@stale_client_timeout = opts.delete(:stale_client_timeout)
|
20
22
|
@redis = opts.delete(:redis) || Redis.new(opts)
|
@@ -23,13 +25,19 @@ class Redis
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def exists_or_create!
|
26
|
-
token = @redis.getset(exists_key,
|
28
|
+
token = @redis.getset(exists_key, EXISTS_TOKEN)
|
27
29
|
|
28
30
|
if token.nil?
|
29
31
|
create!
|
30
|
-
elsif token != API_VERSION
|
31
|
-
raise "Semaphore exists but running as wrong version (version #{token} vs #{API_VERSION})."
|
32
32
|
else
|
33
|
+
# Previous versions of redis-semaphore did not set `version_key`.
|
34
|
+
# Make sure it's set now, so we can use it in future versions.
|
35
|
+
|
36
|
+
if token == API_VERSION && @redis.get(version_key).nil?
|
37
|
+
@redis.set(version_key, API_VERSION)
|
38
|
+
end
|
39
|
+
|
40
|
+
set_expiration_if_necessary
|
33
41
|
true
|
34
42
|
end
|
35
43
|
end
|
@@ -42,6 +50,7 @@ class Redis
|
|
42
50
|
@redis.del(available_key)
|
43
51
|
@redis.del(grabbed_key)
|
44
52
|
@redis.del(exists_key)
|
53
|
+
@redis.del(version_key)
|
45
54
|
end
|
46
55
|
|
47
56
|
def lock(timeout = 0)
|
@@ -80,7 +89,7 @@ class Redis
|
|
80
89
|
@tokens.each do |token|
|
81
90
|
return true if locked?(token)
|
82
91
|
end
|
83
|
-
|
92
|
+
|
84
93
|
false
|
85
94
|
end
|
86
95
|
end
|
@@ -127,6 +136,7 @@ class Redis
|
|
127
136
|
end
|
128
137
|
|
129
138
|
private
|
139
|
+
|
130
140
|
def simple_mutex(key_name, expires = nil)
|
131
141
|
key_name = namespaced_key(key_name) if key_name.kind_of? Symbol
|
132
142
|
token = @redis.getset(key_name, API_VERSION)
|
@@ -150,10 +160,18 @@ class Redis
|
|
150
160
|
@resource_count.times do |index|
|
151
161
|
@redis.rpush(available_key, index)
|
152
162
|
end
|
163
|
+
@redis.set(version_key, API_VERSION)
|
164
|
+
@redis.persist(exists_key)
|
153
165
|
|
154
|
-
|
155
|
-
|
156
|
-
|
166
|
+
set_expiration_if_necessary
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def set_expiration_if_necessary
|
171
|
+
if @expiration
|
172
|
+
[available_key, exists_key, version_key].each do |key|
|
173
|
+
@redis.expire(key, @expiration)
|
174
|
+
end
|
157
175
|
end
|
158
176
|
end
|
159
177
|
|
@@ -185,12 +203,16 @@ class Redis
|
|
185
203
|
@grabbed_key ||= namespaced_key('GRABBED')
|
186
204
|
end
|
187
205
|
|
206
|
+
def version_key
|
207
|
+
@version_key ||= namespaced_key('VERSION')
|
208
|
+
end
|
209
|
+
|
188
210
|
def current_time
|
189
211
|
if @use_local_time
|
190
212
|
Time.now
|
191
213
|
else
|
192
214
|
begin
|
193
|
-
instant = @redis.time
|
215
|
+
instant = redis_namespace? ? @redis.redis.time : @redis.time
|
194
216
|
Time.at(instant[0], instant[1])
|
195
217
|
rescue
|
196
218
|
@use_local_time = true
|
data/spec/semaphore_spec.rb
CHANGED
@@ -112,6 +112,29 @@ describe "redis" do
|
|
112
112
|
end
|
113
113
|
expect(block_value).to eq(lock_token)
|
114
114
|
end
|
115
|
+
|
116
|
+
it "should disappear without a trace when calling `delete!`" do
|
117
|
+
original_key_size = @redis.keys.count
|
118
|
+
|
119
|
+
semaphore.exists_or_create!
|
120
|
+
semaphore.delete!
|
121
|
+
|
122
|
+
expect(@redis.keys.count).to eq(original_key_size)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "semaphore with expiration" do
|
127
|
+
let(:semaphore) { Redis::Semaphore.new(:my_semaphore, :redis => @redis, :expiration => 2) }
|
128
|
+
let(:multisem) { Redis::Semaphore.new(:my_semaphore_2, :resources => 2, :redis => @redis, :expiration => 2) }
|
129
|
+
|
130
|
+
it_behaves_like "a semaphore"
|
131
|
+
|
132
|
+
it "expires keys" do
|
133
|
+
original_key_size = @redis.keys.count
|
134
|
+
semaphore.exists_or_create!
|
135
|
+
sleep 3.0
|
136
|
+
expect(@redis.keys.count).to eq(original_key_size)
|
137
|
+
end
|
115
138
|
end
|
116
139
|
|
117
140
|
describe "semaphore without staleness checking" do
|
@@ -201,4 +224,23 @@ describe "redis" do
|
|
201
224
|
expect(available_keys).to eq(grabbed_keys)
|
202
225
|
end
|
203
226
|
end
|
227
|
+
|
228
|
+
describe "version" do
|
229
|
+
context "with an existing versionless semaphore" do
|
230
|
+
let(:old_sem) { Redis::Semaphore.new(:my_semaphore, :redis => @redis) }
|
231
|
+
let(:semaphore) { Redis::Semaphore.new(:my_semaphore, :redis => @redis) }
|
232
|
+
let(:version_key) { old_sem.send(:version_key) }
|
233
|
+
|
234
|
+
before do
|
235
|
+
old_sem.exists_or_create!
|
236
|
+
@redis.del(version_key)
|
237
|
+
end
|
238
|
+
|
239
|
+
it "sets the version key" do
|
240
|
+
semaphore.exists_or_create!
|
241
|
+
expect(@redis.get(version_key)).not_to be_nil
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
204
246
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require 'bundler'
|
3
|
-
Bundler.
|
4
|
-
Bundler.require(:default, :test)
|
5
|
-
|
6
|
-
require 'rspec'
|
7
|
-
require 'redis'
|
8
|
-
require 'logger'
|
2
|
+
require 'bundler/setup'
|
3
|
+
Bundler.require(:development)
|
9
4
|
|
10
5
|
$TESTING=true
|
11
6
|
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-semaphore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Verhasselt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -24,6 +24,62 @@ dependencies:
|
|
24
24
|
- - ! '>='
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.14'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.14'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
27
83
|
description: ! 'Implements a distributed semaphore or mutex using Redis.
|
28
84
|
|
29
85
|
'
|