redis-semaphore 0.2.4 → 0.3.1

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 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: