redis-semaphore 0.2.4 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MjdjZjZhZWZiNmZiYTY1YWJkMWQ0ZDUyYmJjZGY2NGViMzQzZDhmMA==
5
- data.tar.gz: !binary |-
6
- MjI5YzlmMzBhOWE2ZmUwZTQyNjQ0Y2JjM2ZkMzZhOWEzMTc0YWZkOA==
2
+ SHA1:
3
+ metadata.gz: 8b9f39ef8423ccca320813836891182abf1d93ec
4
+ data.tar.gz: f631ca62601208fe26125808c049abe755de3ad9
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- YzZjOTA0ZWQ2OWUzMzk2YTYyODA1ZWIzYjBiZmU1MTc5Yjg4MTk0MDZjYWYz
10
- YjZiZTZlZTBmYzQ0MWFlNjFlNzFiODVjOTc4YjAyOWRiODUwYWY4MjNiNmYy
11
- ZDAyZDEwOTZjZjk3ODVmMWIyNmJkNWZiYmEyMzM4ZDNjYmU2MTI=
12
- data.tar.gz: !binary |-
13
- MTdhN2RjYTRjMDIxMGJhNTM4YWM0YTViYzNjM2FjYmJmMjBmYjgwZGZjMDE0
14
- MGNiZmM3YjI3OTJiNmU5OTAyZTZhNWVmOGFlNzQyZjMxNTYwZmIzMDY2NGU3
15
- NTA4NTBkMGNiZjRhNmQzZjEyNjM4ZjMwNzFjYTI4YzgxZWFiYzg=
6
+ metadata.gz: 3c38139829552918fbc64bef11059fd3ac8a8117d6f25bbda7bfac2d355c791232d9a2b9c5458cc173d53b8d210b0aa01efd61e8c7aff6e1a5dc22253a6c1070
7
+ data.tar.gz: 0be29fe2f67e984c6e2757c0da6df45eba3ec8a97702cae4a623e185407e347f0642f624338ec66213c012e9f26220c71f4155e26cc1b9bd8a65ba65381689f9
data/README.md CHANGED
@@ -10,6 +10,23 @@ The mutex and semaphore is blocking, not polling, and has a fair queue serving p
10
10
 
11
11
  For more info see [Wikipedia](http://en.wikipedia.org/wiki/Semaphore_(programming)).
12
12
 
13
+ Important change in v0.3.0
14
+ ===========================
15
+
16
+ If you've been using `redis-semaphore` before version `0.3.0` you should be aware that the interface for `lock` has changed slightly. Before `0.3` calling `semaphore.lock(0)` (with `0` as the timeout) would block the semaphore indefinitely, just like a redis `blpop` command would.
17
+
18
+ This has changed in `0.3` to mean *do not block at all*. You can still omit the argument entirely, or pass in `nil` to get the old functionality back. Examples:
19
+
20
+ ```ruby
21
+ # These block indefinitely until a resource becomes available:
22
+ semaphore.lock
23
+ semaphore.lock(nil)
24
+
25
+ # This does not block at all and rather returns immediately if there's no
26
+ # resource available:
27
+ semaphore.lock(0)
28
+ ```
29
+
13
30
  Usage
14
31
  -----
15
32
 
@@ -81,6 +98,8 @@ s = Redis::Semaphore.new(:another_name, :redis => r)
81
98
  #...
82
99
  ```
83
100
 
101
+ Note that it's [a bad idea to reuse the same redis client across threads](https://github.com/dv/redis-semaphore/issues/18), due to the blocking nature of the `blpop` command. We might add support for this in a future version.
102
+
84
103
  If an exception happens during a lock, the lock will automatically be released:
85
104
 
86
105
  ```ruby
@@ -200,54 +219,21 @@ Testing
200
219
  Changelog
201
220
  ---------
202
221
 
222
+ ###0.3.1 April 17, 2016 (Pending)
223
+ - Fix `sem.lock(0)` bug (thanks eugenk!).
224
+ - Fix `release_stale_locks!` deadlock bug (thanks mfischer-zd for the bug-report!).
225
+
226
+ ###0.3.0 January 24, 2016
227
+ - Change API to include non-blocking option for `#lock` (thanks tomclose!).
228
+ - Fix unwanted persisting of `available_key` (thanks dany1468!).
229
+ - Fix `available_count` returning 0 for nonexisting semaphores (thanks mikeryz!).
230
+
203
231
  ###0.2.4 January 11, 2015
204
232
  - Fix bug with TIME and redis-namespace (thanks sos4nt!).
205
233
  - Add expiration option (thanks jcalvert!).
234
+ - Update API version logic.
206
235
 
207
- ###0.2.3 September 7, 2014
208
- - Block-based locking return the value of the block (thanks frobcode!).
209
-
210
- ###0.2.2 June 16, 2014
211
- - Fixed bug in `all_tokens` (thanks presskey!).
212
- - Fixed bug in error message (thanks Dmitriy!).
213
-
214
- ###0.2.1 August 6, 2013
215
- - Remove dependency on Redis 2.6+ using fallback for TIME command (thanks dubdromic!).
216
- - Add ```:use_local_time``` option
217
-
218
- ###0.2.0 June 2, 2013
219
- - Use Redis TIME command for lock timeouts (thanks dubdromic!).
220
- - Version increase because of new dependency on Redis 2.6+
221
-
222
- ###0.1.7 April 18, 2013
223
- - Fix bug where ```release_stale_locks!``` was not public (thanks scomma!).
224
-
225
- ###0.1.6 March 31, 2013
226
- - Add non-ownership of tokens
227
- - Add stale client timeout (thanks timgaleckas!).
228
-
229
- ###0.1.5 October 1, 2012
230
- - Add detection of Redis::Namespace definition to avoid potential bug (thanks ruud!).
231
-
232
- ###0.1.4 October 1, 2012
233
- - Fixed empty namespaces (thanks ruurd!).
234
-
235
- ###0.1.3 July 9, 2012
236
- - Tokens are now identifiable (thanks timgaleckas!).
237
-
238
- ###0.1.2 June 1, 2012
239
- - Add redis-namespace support (thanks neovintage!).
240
-
241
- ### 0.1.1 September 17, 2011
242
- - When an exception is raised during locked period, ensure it unlocks.
243
-
244
- ### 0.1.0 August 4, 2011
245
- - Initial release.
246
-
247
- Author
248
- ------
249
-
250
- [David Verhasselt](http://davidverhasselt.com) - david@crowdway.com
236
+ More in [CHANGELOG](CHANGELOG.md).
251
237
 
252
238
  Contributors
253
239
  ------------
@@ -266,4 +252,11 @@ Thanks to these awesome people for their contributions:
266
252
  - [Petteri Räty](https://github.com/betelgeuse)
267
253
  - [Stefan Schüßler](https://github.com/sos4nt)
268
254
  - [Jonathan Calvert](https://github.com/jcalvert)
255
+ - [mikeryz](https://github.com/mikeryz)
256
+ - [tomclose](https://github.com/tomclose)
257
+ - [Eugen Kuksa](https://github.com/eugenk)
258
+ - [Eugene Kenny](https://github.com/eugeneius)
259
+
260
+ ### "Merge"-button clicker
269
261
 
262
+ [David Verhasselt](http://davidverhasselt.com) - david@crowdway.com
@@ -37,13 +37,16 @@ class Redis
37
37
  @redis.set(version_key, API_VERSION)
38
38
  end
39
39
 
40
- set_expiration_if_necessary
41
40
  true
42
41
  end
43
42
  end
44
43
 
45
44
  def available_count
46
- @redis.llen(available_key)
45
+ if exists?
46
+ @redis.llen(available_key)
47
+ else
48
+ @resource_count
49
+ end
47
50
  end
48
51
 
49
52
  def delete!
@@ -53,14 +56,19 @@ class Redis
53
56
  @redis.del(version_key)
54
57
  end
55
58
 
56
- def lock(timeout = 0)
59
+ def lock(timeout = nil)
57
60
  exists_or_create!
58
61
  release_stale_locks! if check_staleness?
59
62
 
60
- token_pair = @redis.blpop(available_key, timeout)
61
- return false if token_pair.nil?
63
+ if timeout.nil? || timeout > 0
64
+ # passing timeout 0 to blpop causes it to block
65
+ _key, current_token = @redis.blpop(available_key, timeout || 0)
66
+ else
67
+ current_token = @redis.lpop(available_key)
68
+ end
69
+
70
+ return false if current_token.nil?
62
71
 
63
- current_token = token_pair[1]
64
72
  @tokens.push(current_token)
65
73
  @redis.hset(grabbed_key, current_token, current_time.to_f)
66
74
  return_value = current_token
@@ -100,6 +108,8 @@ class Redis
100
108
  @redis.multi do
101
109
  @redis.hdel grabbed_key, token
102
110
  @redis.lpush available_key, token
111
+
112
+ set_expiration_if_necessary
103
113
  end
104
114
  end
105
115
 
@@ -124,7 +134,7 @@ class Redis
124
134
  end
125
135
 
126
136
  def release_stale_locks!
127
- simple_mutex(:release_locks, 10) do
137
+ simple_expiring_mutex(:release_locks, 10) do
128
138
  @redis.hgetall(grabbed_key).each do |token, locked_at|
129
139
  timed_out_at = locked_at.to_f + @stale_client_timeout
130
140
 
@@ -137,17 +147,37 @@ class Redis
137
147
 
138
148
  private
139
149
 
140
- def simple_mutex(key_name, expires = nil)
141
- key_name = namespaced_key(key_name) if key_name.kind_of? Symbol
142
- token = @redis.getset(key_name, API_VERSION)
150
+ def simple_expiring_mutex(key_name, expires_in)
151
+ # Using the locking mechanism as described in
152
+ # http://redis.io/commands/setnx
153
+
154
+ key_name = namespaced_key(key_name)
155
+ cached_current_time = current_time.to_f
156
+ my_lock_expires_at = cached_current_time + expires_in + 1
157
+
158
+ got_lock = @redis.setnx(key_name, my_lock_expires_at)
159
+
160
+ if !got_lock
161
+ # Check if expired
162
+ other_lock_expires_at = @redis.get(key_name).to_f
163
+
164
+ if other_lock_expires_at < cached_current_time
165
+ old_expires_at = @redis.getset(key_name, my_lock_expires_at).to_f
166
+
167
+ # Check if another client started cleanup yet. If not,
168
+ # then we now have the lock.
169
+ got_lock = (old_expires_at == other_lock_expires_at)
170
+ end
171
+ end
143
172
 
144
- return false unless token.nil?
145
- @redis.expire(key_name, expires) unless expires.nil?
173
+ return false if !got_lock
146
174
 
147
175
  begin
148
- yield token
176
+ yield
149
177
  ensure
150
- @redis.del(key_name)
178
+ # Make sure not to delete the lock in case someone else already expired
179
+ # our lock, with one second in between to account for some lag.
180
+ @redis.del(key_name) if my_lock_expires_at > (current_time.to_f - 1)
151
181
  end
152
182
  end
153
183
 
@@ -22,6 +22,10 @@ describe "redis" do
22
22
  expect(semaphore.available_count).to eq(1)
23
23
  end
24
24
 
25
+ it "has the correct amount of available resources before locking" do
26
+ expect(semaphore.available_count).to eq(1)
27
+ end
28
+
25
29
  it "should not exist from the start" do
26
30
  expect(semaphore.exists?).to eq(false)
27
31
  semaphore.lock
@@ -89,9 +93,9 @@ describe "redis" do
89
93
  it "should not leave the semaphore locked after raising an exception" do
90
94
  expect {
91
95
  semaphore.lock(1) do
92
- raise Exception
96
+ raise Exception, "redis semaphore exception"
93
97
  end
94
- }.to raise_error
98
+ }.to raise_error(Exception, "redis semaphore exception")
95
99
 
96
100
  expect(semaphore.locked?).to eq(false)
97
101
  end
@@ -121,6 +125,24 @@ describe "redis" do
121
125
 
122
126
  expect(@redis.keys.count).to eq(original_key_size)
123
127
  end
128
+
129
+ it "should not block when the timeout is zero" do
130
+ did_we_get_in = false
131
+
132
+ semaphore.lock do
133
+ semaphore.lock(0) do
134
+ did_we_get_in = true
135
+ end
136
+ end
137
+
138
+ expect(did_we_get_in).to be false
139
+ end
140
+
141
+ it "should be locked when the timeout is zero" do
142
+ semaphore.lock(0) do
143
+ expect(semaphore.locked?).to be true
144
+ end
145
+ end
124
146
  end
125
147
 
126
148
  describe "semaphore with expiration" do
@@ -135,6 +157,15 @@ describe "redis" do
135
157
  sleep 3.0
136
158
  expect(@redis.keys.count).to eq(original_key_size)
137
159
  end
160
+
161
+ it "expires keys after unlocking" do
162
+ original_key_size = @redis.keys.count
163
+ semaphore.lock do
164
+ # noop
165
+ end
166
+ sleep 3.0
167
+ expect(@redis.keys.count).to eq(original_key_size)
168
+ end
138
169
  end
139
170
 
140
171
  describe "semaphore without staleness checking" do
@@ -243,4 +274,37 @@ describe "redis" do
243
274
  end
244
275
  end
245
276
 
277
+ # Private method tests, do not use
278
+ describe "simple_expiring_mutex" do
279
+ let(:semaphore) { Redis::Semaphore.new(:my_semaphore, :redis => @redis) }
280
+
281
+ before do
282
+ semaphore.class.send(:public, :simple_expiring_mutex)
283
+ end
284
+
285
+ it "gracefully expires stale lock" do
286
+ expiration = 1
287
+
288
+ thread =
289
+ Thread.new do
290
+ semaphore.simple_expiring_mutex(:test, expiration) do
291
+ sleep 3
292
+ end
293
+ end
294
+
295
+ sleep 1.5
296
+
297
+ expect(semaphore.simple_expiring_mutex(:test, expiration)).to be_falsy
298
+
299
+ sleep expiration
300
+
301
+ it_worked = false
302
+ semaphore.simple_expiring_mutex(:test, expiration) do
303
+ it_worked = true
304
+ end
305
+
306
+ expect(it_worked).to be_truthy
307
+ thread.join
308
+ end
309
+ end
246
310
  end
data/spec/spec_helper.rb CHANGED
@@ -5,3 +5,8 @@ Bundler.require(:development)
5
5
  $TESTING=true
6
6
  $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
7
7
  require 'redis/semaphore'
8
+
9
+ RSpec.configure do |c|
10
+ c.filter_run focus: true
11
+ c.run_all_when_everything_filtered = true
12
+ end
metadata CHANGED
@@ -1,98 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-semaphore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Verhasselt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-11 00:00:00.000000000 Z
11
+ date: 2016-05-14 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
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - "<"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '11'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - "<"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '11'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '2.14'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.14'
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry
56
+ name: timecop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: timecop
70
+ name: pry
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ! '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ! '>='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- description: ! 'Implements a distributed semaphore or mutex using Redis.
84
-
85
- '
83
+ description: |
84
+ Implements a distributed semaphore or mutex using Redis.
86
85
  email: david@crowdway.com
87
86
  executables: []
88
87
  extensions: []
89
88
  extra_rdoc_files: []
90
89
  files:
90
+ - LICENSE
91
91
  - README.md
92
92
  - Rakefile
93
- - LICENSE
94
- - lib/redis/semaphore.rb
95
93
  - lib/redis-semaphore.rb
94
+ - lib/redis/semaphore.rb
96
95
  - spec/semaphore_spec.rb
97
96
  - spec/spec_helper.rb
98
97
  homepage: http://github.com/dv/redis-semaphore
@@ -105,19 +104,18 @@ require_paths:
105
104
  - lib
106
105
  required_ruby_version: !ruby/object:Gem::Requirement
107
106
  requirements:
108
- - - ! '>='
107
+ - - ">="
109
108
  - !ruby/object:Gem::Version
110
109
  version: '0'
111
110
  required_rubygems_version: !ruby/object:Gem::Requirement
112
111
  requirements:
113
- - - ! '>='
112
+ - - ">="
114
113
  - !ruby/object:Gem::Version
115
114
  version: '0'
116
115
  requirements: []
117
116
  rubyforge_project:
118
- rubygems_version: 2.1.10
117
+ rubygems_version: 2.4.5.1
119
118
  signing_key:
120
119
  specification_version: 4
121
120
  summary: Implements a distributed semaphore or mutex using Redis.
122
121
  test_files: []
123
- has_rdoc: