redis-semaphore 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +97 -18
- data/Rakefile +1 -1
- data/lib/redis/semaphore.rb +141 -44
- data/spec/semaphore_spec.rb +137 -0
- metadata +3 -3
- data/spec/redis_spec.rb +0 -106
data/README.md
CHANGED
@@ -3,14 +3,14 @@ redis-semaphore
|
|
3
3
|
|
4
4
|
Implements a mutex and semaphore using Redis and the neat BLPOP command.
|
5
5
|
|
6
|
-
The mutex and semaphore is blocking, not polling, and has a fair queue serving processes on a first-come, first-serve basis.
|
6
|
+
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.
|
7
7
|
|
8
8
|
For more info see [Wikipedia](http://en.wikipedia.org/wiki/Semaphore_(programming\)).
|
9
9
|
|
10
10
|
Usage
|
11
11
|
-----
|
12
12
|
|
13
|
-
|
13
|
+
Create a mutex:
|
14
14
|
|
15
15
|
```ruby
|
16
16
|
s = Redis::Semaphore.new(:semaphore_name, :connection => "localhost")
|
@@ -18,37 +18,37 @@ s.lock do
|
|
18
18
|
# We're now in a mutex protected area
|
19
19
|
# No matter how many processes are running this program,
|
20
20
|
# there will be only one running this code block at a time.
|
21
|
-
|
21
|
+
work
|
22
22
|
end
|
23
23
|
```
|
24
24
|
|
25
25
|
While our application is inside the code block given to ```s.lock```, other calls to use the mutex with the same name will block until our code block is finished. Once our mutex unlocks, the next process will unblock and be able to execute the code block. The blocking processes get unblocked in order of arrival, creating a fair queue.
|
26
26
|
|
27
|
-
You can also allow a set number of processes inside the semaphore-protected block:
|
27
|
+
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:
|
28
28
|
|
29
29
|
```ruby
|
30
|
-
s = Redis::Semaphore.new(:semaphore_name, 5, :connection => "localhost")
|
30
|
+
s = Redis::Semaphore.new(:semaphore_name, :resources => 5, :connection => "localhost")
|
31
31
|
s.lock do
|
32
32
|
# Up to five processes at a time will be able to get inside this code
|
33
33
|
# block simultaneously.
|
34
|
-
|
34
|
+
work
|
35
35
|
end
|
36
36
|
```
|
37
37
|
|
38
|
-
You
|
38
|
+
You're not obligated to use code blocks, linear calls work just fine:
|
39
39
|
|
40
40
|
```ruby
|
41
41
|
s = Redis::Semaphore.new(:semaphore_name, :connection => "localhost")
|
42
42
|
s.lock
|
43
|
-
|
44
|
-
s.unlock # Don't forget this, or the mutex will
|
43
|
+
work
|
44
|
+
s.unlock # Don't forget this, or the mutex will stay locked!
|
45
45
|
```
|
46
46
|
|
47
|
-
If you don't want to wait forever until the
|
47
|
+
If you don't want to wait forever until the semaphore releases, you can pass in a timeout of seconds you want to wait:
|
48
48
|
|
49
49
|
```ruby
|
50
|
-
if s.lock(5) # This will only block for at most 5 seconds if the
|
51
|
-
|
50
|
+
if s.lock(5) # This will only block for at most 5 seconds if the semaphore stays locked.
|
51
|
+
work
|
52
52
|
s.unlock
|
53
53
|
else
|
54
54
|
puts "Aborted."
|
@@ -58,15 +58,23 @@ end
|
|
58
58
|
You can check if the mutex or semaphore already exists, or how many resources are left in the semaphore:
|
59
59
|
|
60
60
|
```ruby
|
61
|
-
puts "
|
62
|
-
puts "There are #{s.
|
61
|
+
puts "This semaphore does exist." if s.exists?
|
62
|
+
puts "There are #{s.available_count} resources available right now."
|
63
|
+
```
|
64
|
+
|
65
|
+
When calling ```unlock```, the new number of available resources is returned:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
sem.lock
|
69
|
+
sem.unlock # returns 1
|
70
|
+
sem.available_count # also returns 1
|
63
71
|
```
|
64
72
|
|
65
73
|
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:
|
66
74
|
|
67
75
|
```ruby
|
68
76
|
r = Redis.new(:connection => "localhost", :db => 222)
|
69
|
-
s = Redis::Semaphore.new(:another_name, r)
|
77
|
+
s = Redis::Semaphore.new(:another_name, :redis => r)
|
70
78
|
#...
|
71
79
|
```
|
72
80
|
|
@@ -83,6 +91,73 @@ end
|
|
83
91
|
```
|
84
92
|
|
85
93
|
|
94
|
+
Staleness
|
95
|
+
---------
|
96
|
+
|
97
|
+
To allow for clients to die, and the token returned to the list, a stale-check was added. As soon as a lock is started, the time of the lock is set. If another process detects that the timeout has passed since the lock was set, it can force unlock the lock itself.
|
98
|
+
|
99
|
+
There are two ways to take advantage of this. You can either define a :stale\_client\_timeout upon initialization. This will check for stale locks everytime your program wants to lock the semaphore:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
s = Redis::Semaphore(:stale_semaphore, :redis = r, :stale_client_timeout => 1000) # in ms
|
103
|
+
```
|
104
|
+
|
105
|
+
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:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
normal_sem = Redis::Semaphore(:semaphore, :connection => "localhost")
|
109
|
+
|
110
|
+
Thread.new do
|
111
|
+
watchdog = Redis::Semaphore(:semaphore, :connection => "localhost", :stale_client_timeout => 1000)
|
112
|
+
|
113
|
+
while(true) do
|
114
|
+
watchdog.release_stale_locks!
|
115
|
+
sleep 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
normal_sem.lock
|
120
|
+
sleep 5
|
121
|
+
normal_sem.locked? # returns false
|
122
|
+
|
123
|
+
normal_sem.lock
|
124
|
+
normal_sem.lock(5) # will block until the watchdog releases the previous lock after 1 second
|
125
|
+
```
|
126
|
+
|
127
|
+
|
128
|
+
Advanced
|
129
|
+
--------
|
130
|
+
|
131
|
+
The methods ```wait``` and ```signal```, the traditional method names of a Semaphore, are also implemented. ```wait``` is aliased to lock, while ```signal``` puts the specified token back on the semaphore, or generates a unique new token and puts that back if none is passed:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
# Retrieve 2 resources
|
135
|
+
token1 = sem.wait
|
136
|
+
token2 = sem.wait
|
137
|
+
|
138
|
+
work
|
139
|
+
|
140
|
+
# Put 3 resources back
|
141
|
+
sem.signal(token1)
|
142
|
+
sem.signal(token2)
|
143
|
+
sem.signal
|
144
|
+
|
145
|
+
sem.available_count # returns 3
|
146
|
+
```
|
147
|
+
|
148
|
+
This can be used to create a semaphore where the process that consumes resources, and the process that generates resources, are not the same. An example is a dynamic queue system with a consumer process and a producer process:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# Consumer process
|
152
|
+
job = semaphore.wait
|
153
|
+
|
154
|
+
# Producer process
|
155
|
+
semaphore.signal(new_job) # Job can be any string, it will be passed unmodified to the consumer process
|
156
|
+
```
|
157
|
+
|
158
|
+
Used in this fashion, a timeout does not make sense. Using the :stale\_client\_timeout here is not recommended.
|
159
|
+
|
160
|
+
|
86
161
|
Installation
|
87
162
|
------------
|
88
163
|
|
@@ -97,6 +172,10 @@ Testing
|
|
97
172
|
Changelog
|
98
173
|
---------
|
99
174
|
|
175
|
+
###0.1.6 March 31, 2013
|
176
|
+
- Add non-ownership of tokens
|
177
|
+
- Add stale client timeout (thanks timgaleckas!)
|
178
|
+
|
100
179
|
###0.1.5 October 1, 2012
|
101
180
|
- Add detection of Redis::Namespace definition to avoid potential bug (thanks ruud!).
|
102
181
|
|
@@ -125,6 +204,6 @@ Contributors
|
|
125
204
|
|
126
205
|
Thanks to these awesome fellas for their contributions:
|
127
206
|
|
128
|
-
-
|
129
|
-
-
|
130
|
-
-
|
207
|
+
- [Rimas Silkaitis](https://github.com/neovintage)
|
208
|
+
- [Tim Galeckas](https://github.com/timgaleckas)
|
209
|
+
- [Ruurd Pels](https://github.com/ruurd)
|
data/Rakefile
CHANGED
data/lib/redis/semaphore.rb
CHANGED
@@ -2,88 +2,185 @@ require 'redis'
|
|
2
2
|
|
3
3
|
class Redis
|
4
4
|
class Semaphore
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@
|
22
|
-
|
5
|
+
API_VERSION = "1"
|
6
|
+
|
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.
|
10
|
+
# Default is nil for which we don't check for stale clients
|
11
|
+
# Redis::Semaphore.new(:my_semaphore, :stale_client_timeout => 30, :redis => myRedis)
|
12
|
+
# Redis::Semaphore.new(:my_semaphore, :redis => myRedis)
|
13
|
+
# Redis::Semaphore.new(:my_semaphore, :resources => 1, :redis => myRedis)
|
14
|
+
# Redis::Semaphore.new(:my_semaphore, :connection => "", :port => "")
|
15
|
+
# Redis::Semaphore.new(:my_semaphore, :path => "bla")
|
16
|
+
def initialize(name, opts = {})
|
17
|
+
@name = name
|
18
|
+
@resource_count = opts.delete(:resources) || 1
|
19
|
+
@stale_client_timeout = opts.delete(:stale_client_timeout)
|
20
|
+
@redis = opts.delete(:redis) || Redis.new(opts)
|
21
|
+
@tokens = []
|
23
22
|
end
|
24
23
|
|
25
|
-
def
|
26
|
-
@redis.
|
24
|
+
def exists_or_create!
|
25
|
+
token = @redis.getset(exists_key, API_VERSION)
|
26
|
+
|
27
|
+
if token.nil?
|
28
|
+
create!
|
29
|
+
elsif token != API_VERSION
|
30
|
+
raise "Semaphore exists but running as wrong version (version #{version} vs #{API_VERSION})."
|
31
|
+
else
|
32
|
+
true
|
33
|
+
end
|
27
34
|
end
|
28
35
|
|
29
|
-
def
|
30
|
-
@redis.
|
36
|
+
def available_count
|
37
|
+
@redis.llen(available_key)
|
31
38
|
end
|
32
39
|
|
33
40
|
def delete!
|
34
|
-
@redis.del(
|
35
|
-
@redis.del(
|
41
|
+
@redis.del(available_key)
|
42
|
+
@redis.del(grabbed_key)
|
43
|
+
@redis.del(exists_key)
|
36
44
|
end
|
37
45
|
|
38
46
|
def lock(timeout = 0)
|
39
47
|
exists_or_create!
|
48
|
+
release_stale_locks! if check_staleness?
|
40
49
|
|
50
|
+
token_pair = @redis.blpop(available_key, timeout)
|
51
|
+
return false if token_pair.nil?
|
41
52
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
53
|
+
current_token = token_pair[1]
|
54
|
+
@tokens.push(current_token)
|
55
|
+
@redis.hset(grabbed_key, current_token, Time.now.to_i)
|
56
|
+
|
46
57
|
if block_given?
|
47
58
|
begin
|
48
|
-
yield
|
59
|
+
yield current_token
|
49
60
|
ensure
|
50
|
-
|
61
|
+
signal(current_token)
|
51
62
|
end
|
52
63
|
end
|
53
64
|
|
54
|
-
|
65
|
+
current_token
|
55
66
|
end
|
67
|
+
alias_method :wait, :lock
|
56
68
|
|
57
69
|
def unlock
|
58
70
|
return false unless locked?
|
71
|
+
signal(@tokens.pop)[1]
|
72
|
+
end
|
59
73
|
|
60
|
-
|
61
|
-
|
74
|
+
def locked?(token = nil)
|
75
|
+
if token
|
76
|
+
@redis.hexists(grabbed_key, token)
|
77
|
+
else
|
78
|
+
@tokens.each do |token|
|
79
|
+
return true if locked?(token)
|
80
|
+
end
|
81
|
+
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def signal(token = 1)
|
87
|
+
token ||= generate_unique_token
|
88
|
+
|
89
|
+
@redis.multi do
|
90
|
+
@redis.hdel grabbed_key, token
|
91
|
+
@redis.lpush available_key, token
|
92
|
+
end
|
62
93
|
end
|
63
94
|
|
64
|
-
def
|
65
|
-
|
95
|
+
def exists?
|
96
|
+
@redis.exists(exists_key)
|
97
|
+
end
|
98
|
+
|
99
|
+
def all_tokens
|
100
|
+
@redis.multi do
|
101
|
+
@redis.lrange(available_key, 0, -1)
|
102
|
+
@redis.lrange(grabbed_key, 0, -1)
|
103
|
+
end.flatten
|
66
104
|
end
|
67
105
|
|
106
|
+
def generate_unique_token
|
107
|
+
tokens = all_tokens
|
108
|
+
token = Random.rand.to_s
|
109
|
+
|
110
|
+
while(tokens.include? token)
|
111
|
+
token = Random.rand.to_s
|
112
|
+
end
|
113
|
+
end
|
68
114
|
|
69
115
|
private
|
70
|
-
def
|
71
|
-
(
|
116
|
+
def simple_mutex(key_name, expires = nil)
|
117
|
+
key_name = namespaced_key(key_name) if key_name.kind_of? Symbol
|
118
|
+
token = @redis.getset(key_name, API_VERSION)
|
119
|
+
|
120
|
+
return false unless token.nil?
|
121
|
+
@redis.expire(key_name, expires) unless expires.nil?
|
122
|
+
|
123
|
+
begin
|
124
|
+
yield token
|
125
|
+
ensure
|
126
|
+
@redis.del(key_name)
|
127
|
+
end
|
72
128
|
end
|
73
129
|
|
74
|
-
def
|
75
|
-
(
|
130
|
+
def release_stale_locks!
|
131
|
+
simple_mutex(:release_locks, 10) do
|
132
|
+
@redis.hgetall(grabbed_key).each do |token, locked_at|
|
133
|
+
timed_out_at = locked_at.to_i + @stale_client_timeout
|
134
|
+
|
135
|
+
if timed_out_at < Time.now.to_i
|
136
|
+
signal(token)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
76
140
|
end
|
77
141
|
|
78
|
-
def
|
79
|
-
|
142
|
+
def create!
|
143
|
+
@redis.expire(exists_key, 10)
|
80
144
|
|
81
|
-
|
82
|
-
@
|
83
|
-
|
145
|
+
@redis.multi do
|
146
|
+
@redis.del(grabbed_key)
|
147
|
+
@redis.del(available_key)
|
148
|
+
@resource_count.times do |index|
|
149
|
+
@redis.rpush(available_key, index)
|
84
150
|
end
|
151
|
+
|
152
|
+
# Persist key
|
153
|
+
@redis.del(exists_key)
|
154
|
+
@redis.set(exists_key, API_VERSION)
|
85
155
|
end
|
86
156
|
end
|
87
157
|
|
158
|
+
def check_staleness?
|
159
|
+
!@stale_client_timeout.nil?
|
160
|
+
end
|
161
|
+
|
162
|
+
def redis_namespace?
|
163
|
+
(defined?(Redis::Namespace) && @redis.is_a?(Redis::Namespace))
|
164
|
+
end
|
165
|
+
|
166
|
+
def namespaced_key(variable)
|
167
|
+
if redis_namespace?
|
168
|
+
"#{@name}:#{variable}"
|
169
|
+
else
|
170
|
+
"SEMAPHORE:#{@name}:#{variable}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def available_key
|
175
|
+
@available_key ||= namespaced_key('AVAILABLE')
|
176
|
+
end
|
177
|
+
|
178
|
+
def exists_key
|
179
|
+
@exists_key ||= namespaced_key('EXISTS')
|
180
|
+
end
|
181
|
+
|
182
|
+
def grabbed_key
|
183
|
+
@grabbed_key ||= namespaced_key('GRABBED')
|
184
|
+
end
|
88
185
|
end
|
89
186
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe "redis" do
|
4
|
+
before(:all) do
|
5
|
+
# use database 15 for testing so we dont accidentally step on you real data
|
6
|
+
@redis = Redis.new :db => 15
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
@redis.flushdb
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:all) do
|
14
|
+
@redis.quit
|
15
|
+
end
|
16
|
+
|
17
|
+
shared_examples_for "a semaphore" do
|
18
|
+
|
19
|
+
it "has the correct amount of available resources" do
|
20
|
+
semaphore.lock
|
21
|
+
semaphore.unlock.should == 1
|
22
|
+
semaphore.available_count.should == 1
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not exist from the start" do
|
26
|
+
semaphore.exists?.should == false
|
27
|
+
semaphore.lock
|
28
|
+
semaphore.exists?.should == true
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should be unlocked from the start" do
|
32
|
+
semaphore.locked?.should == false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should lock and unlock" do
|
36
|
+
semaphore.lock(1)
|
37
|
+
semaphore.locked?.should == true
|
38
|
+
semaphore.unlock
|
39
|
+
semaphore.locked?.should == false
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not lock twice as a mutex" do
|
43
|
+
semaphore.lock(1).should_not == false
|
44
|
+
semaphore.lock(1).should == false
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should not lock three times when only two available" do
|
48
|
+
multisem.lock(1).should_not == false
|
49
|
+
multisem.lock(1).should_not == false
|
50
|
+
multisem.lock(1).should == false
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should always have the correct lock-status" do
|
54
|
+
multisem.lock(1)
|
55
|
+
multisem.lock(1)
|
56
|
+
|
57
|
+
multisem.locked?.should == true
|
58
|
+
multisem.unlock
|
59
|
+
multisem.locked?.should == true
|
60
|
+
multisem.unlock
|
61
|
+
multisem.locked?.should == false
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should get all different tokens when saturating" do
|
65
|
+
ids = []
|
66
|
+
2.times do
|
67
|
+
ids << multisem.lock(1)
|
68
|
+
end
|
69
|
+
|
70
|
+
ids.should == %w(0 1)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should execute the given code block" do
|
74
|
+
code_executed = false
|
75
|
+
semaphore.lock(1) do
|
76
|
+
code_executed = true
|
77
|
+
end
|
78
|
+
code_executed.should == true
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should pass an exception right through" do
|
82
|
+
lambda do
|
83
|
+
semaphore.lock(1) do
|
84
|
+
raise Exception, "redis semaphore exception"
|
85
|
+
end
|
86
|
+
end.should raise_error(Exception, "redis semaphore exception")
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should not leave the semaphore locked after raising an exception" do
|
90
|
+
lambda do
|
91
|
+
semaphore.lock(1) do
|
92
|
+
raise Exception
|
93
|
+
end
|
94
|
+
end.should raise_error
|
95
|
+
|
96
|
+
semaphore.locked?.should == false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "semaphore without staleness checking" do
|
101
|
+
let(:semaphore) { Redis::Semaphore.new(:my_semaphore, :redis => @redis) }
|
102
|
+
let(:multisem) { Redis::Semaphore.new(:my_semaphore_2, :resources => 2, :redis => @redis) }
|
103
|
+
|
104
|
+
it_behaves_like "a semaphore"
|
105
|
+
|
106
|
+
it "can dynamically add resources" do
|
107
|
+
semaphore.exists_or_create!
|
108
|
+
|
109
|
+
3.times do
|
110
|
+
semaphore.signal
|
111
|
+
end
|
112
|
+
|
113
|
+
semaphore.available_count.should == 4
|
114
|
+
|
115
|
+
semaphore.wait(1)
|
116
|
+
semaphore.wait(1)
|
117
|
+
semaphore.wait(1)
|
118
|
+
|
119
|
+
semaphore.available_count.should == 1
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "semaphore with staleness checking" do
|
124
|
+
let(:semaphore) { Redis::Semaphore.new(:my_semaphore, :redis => @redis, :stale_client_timeout => 5) }
|
125
|
+
let(:multisem) { Redis::Semaphore.new(:my_semaphore_2, :resources => 2, :redis => @redis, :stale_client_timeout => 5) }
|
126
|
+
|
127
|
+
it_behaves_like "a semaphore"
|
128
|
+
|
129
|
+
it "should restore resources of stale clients" do
|
130
|
+
hyper_aggressive_sem = Redis::Semaphore.new(:hyper_aggressive_sem, :resources => 1, :redis => @redis, :stale_client_timeout => 1)
|
131
|
+
|
132
|
+
hyper_aggressive_sem.lock(1).should_not == false
|
133
|
+
hyper_aggressive_sem.lock(1).should == false
|
134
|
+
hyper_aggressive_sem.lock(1).should_not == false
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-semaphore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-03-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -40,7 +40,7 @@ files:
|
|
40
40
|
- LICENSE
|
41
41
|
- lib/redis/semaphore.rb
|
42
42
|
- lib/redis-semaphore.rb
|
43
|
-
- spec/
|
43
|
+
- spec/semaphore_spec.rb
|
44
44
|
- spec/spec_helper.rb
|
45
45
|
homepage: http://github.com/dv/redis-semaphore
|
46
46
|
licenses: []
|
data/spec/redis_spec.rb
DELETED
@@ -1,106 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
-
|
3
|
-
describe "redis" do
|
4
|
-
before(:all) do
|
5
|
-
# use database 15 for testing so we dont accidentally step on you real data
|
6
|
-
@redis = Redis.new :db => 15
|
7
|
-
@semaphore = Redis::Semaphore.new(:my_semaphore, @redis)
|
8
|
-
end
|
9
|
-
|
10
|
-
before(:each) do
|
11
|
-
@redis.flushdb
|
12
|
-
end
|
13
|
-
|
14
|
-
after(:each) do
|
15
|
-
@redis.flushdb
|
16
|
-
end
|
17
|
-
|
18
|
-
after(:all) do
|
19
|
-
@redis.quit
|
20
|
-
end
|
21
|
-
|
22
|
-
it "should be unlocked from the start" do
|
23
|
-
@semaphore.locked?.should == false
|
24
|
-
end
|
25
|
-
|
26
|
-
it "should lock and unlock" do
|
27
|
-
@semaphore.lock
|
28
|
-
@semaphore.locked?.should == true
|
29
|
-
@semaphore.unlock
|
30
|
-
@semaphore.locked?.should == false
|
31
|
-
end
|
32
|
-
|
33
|
-
it "should not lock twice as a mutex" do
|
34
|
-
@semaphore.lock
|
35
|
-
@semaphore.lock(1).should == false
|
36
|
-
end
|
37
|
-
|
38
|
-
it "should not lock three times when only two available" do
|
39
|
-
multisem = Redis::Semaphore.new(:my_semaphore2, 2, @redis)
|
40
|
-
multisem.lock.should == true
|
41
|
-
multisem.lock(1).should == true
|
42
|
-
multisem.lock(1).should == false
|
43
|
-
end
|
44
|
-
|
45
|
-
it "should reuse the same index for 5 calls in serial" do
|
46
|
-
multisem = Redis::Semaphore.new(:my_semaphore5_serial, 5, @redis)
|
47
|
-
ids = []
|
48
|
-
5.times do
|
49
|
-
multisem.lock(1) do |i|
|
50
|
-
ids << i
|
51
|
-
end
|
52
|
-
end
|
53
|
-
ids.size.should == 5
|
54
|
-
ids.uniq.size.should == 1
|
55
|
-
end
|
56
|
-
|
57
|
-
it "should have 5 different indexes for 5 parallel calls" do
|
58
|
-
multisem = Redis::Semaphore.new(:my_semaphore5_parallel, 5, @redis)
|
59
|
-
ids = []
|
60
|
-
multisem.lock(1) do |i|
|
61
|
-
ids << i
|
62
|
-
multisem.lock(1) do |i|
|
63
|
-
ids << i
|
64
|
-
multisem.lock(1) do |i|
|
65
|
-
ids << i
|
66
|
-
multisem.lock(1) do |i|
|
67
|
-
ids << i
|
68
|
-
multisem.lock(1) do |i|
|
69
|
-
ids << i
|
70
|
-
multisem.lock(1) do |i|
|
71
|
-
ids << i
|
72
|
-
end.should == false
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
(0..4).to_a.should == ids
|
79
|
-
end
|
80
|
-
|
81
|
-
it "should execute the given code block" do
|
82
|
-
code_executed = false
|
83
|
-
@semaphore.lock do
|
84
|
-
code_executed = true
|
85
|
-
end
|
86
|
-
code_executed.should == true
|
87
|
-
end
|
88
|
-
|
89
|
-
it "should pass an exception right through" do
|
90
|
-
lambda do
|
91
|
-
@semaphore.lock do
|
92
|
-
raise Exception, "redis semaphore exception"
|
93
|
-
end
|
94
|
-
end.should raise_error(Exception, "redis semaphore exception")
|
95
|
-
end
|
96
|
-
|
97
|
-
it "should not leave the semaphore locked after raising an exception" do
|
98
|
-
lambda do
|
99
|
-
@semaphore.lock do
|
100
|
-
raise Exception
|
101
|
-
end
|
102
|
-
end.should raise_error
|
103
|
-
|
104
|
-
@semaphore.locked?.should == false
|
105
|
-
end
|
106
|
-
end
|