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 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
- First let's see how to create a mutex:
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
- do_something_speshiul()
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
- do_something_less_speshiul()
34
+ work
35
35
  end
36
36
  ```
37
37
 
38
- You don't need to use code blocks, you can also use linear code:
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
- do_something_speshiul()
44
- s.unlock # Don't forget this, or the mutex will be locked forever!
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 mutex or semaphore release, you can use a timeout, in seconds:
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 mutex stays locked.
51
- do_something_speshiul()
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 "Someone already initialized this mutex or semaphore!" if s.exists?
62
- puts "There are #{s.available} resources available right now."
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
- - (Rimas Silkaitis)[https://github.com/neovintage]
129
- - (Tim Galeckas)[https://github.com/timgaleckas]
130
- - (Ruurd Pels)[https://github.com/ruurd]
207
+ - [Rimas Silkaitis](https://github.com/neovintage)
208
+ - [Tim Galeckas](https://github.com/timgaleckas)
209
+ - [Ruurd Pels](https://github.com/ruurd)
data/Rakefile CHANGED
@@ -3,5 +3,5 @@ task :test => :spec
3
3
 
4
4
  desc "Run specs"
5
5
  task :spec do
6
- exec "rspec spec/redis_spec.rb"
6
+ exec "rspec spec/semaphore_spec.rb"
7
7
  end
@@ -2,88 +2,185 @@ require 'redis'
2
2
 
3
3
  class Redis
4
4
  class Semaphore
5
-
6
- attr_reader :resources
7
-
8
- # RedisSempahore.new(:my_semaphore, 5, myRedis)
9
- # RedisSemaphore.new(:my_semaphore, myRedis)
10
- # RedisSemaphore.new(:my_semaphore, :connection => "", :port => "")
11
- # RedisSemaphore.new(:my_semaphore, :path => "bla")
12
- def initialize(*args)
13
- raise "Need at least two arguments" if args.size < 2
14
-
15
- @locked = false
16
- @name = args.shift.to_s
17
- @redis = args.pop
18
- if !(@redis.is_a?(Redis) || (defined?(Redis::Namespace) && @redis.is_a?(Redis::Namespace)))
19
- @redis = Redis.new(@redis)
20
- end
21
- @resources = args.pop || 1
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 available
26
- @redis.llen(list_name)
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 exists?
30
- @redis.exists(exists_name)
36
+ def available_count
37
+ @redis.llen(available_key)
31
38
  end
32
39
 
33
40
  def delete!
34
- @redis.del(list_name)
35
- @redis.del(exists_name)
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
- resource_index = @redis.blpop(list_name, timeout)
43
- return false if resource_index.nil?
44
-
45
- @locked = resource_index[1].to_i
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 @locked
59
+ yield current_token
49
60
  ensure
50
- unlock
61
+ signal(current_token)
51
62
  end
52
63
  end
53
64
 
54
- true
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
- @redis.lpush(list_name, @locked)
61
- @locked = false
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 locked?
65
- !!@locked
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 list_name
71
- (defined?(Redis::Namespace) && @redis.is_a?(Redis::Namespace)) ? "#{@name}:LIST" : "SEMAPHORE:#{@name}:LIST"
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 exists_name
75
- (defined?(Redis::Namespace) && @redis.is_a?(Redis::Namespace)) ? "#{@name}:EXISTS" : "SEMAPHORE:#{@name}:EXISTS"
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 exists_or_create!
79
- exists = @redis.getset(exists_name, 1)
142
+ def create!
143
+ @redis.expire(exists_key, 10)
80
144
 
81
- if "1" != exists
82
- @resources.times do |index|
83
- @redis.rpush(list_name, index)
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.5
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: 2012-10-01 00:00:00.000000000 Z
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/redis_spec.rb
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