redis-em-mutex 0.2.1 → 0.2.2
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.
- data/HISTORY.rdoc +7 -0
- data/{README.rdoc → README.md} +127 -73
- data/Rakefile +1 -1
- data/lib/redis/em-mutex/version.rb +1 -1
- data/lib/redis/em-mutex.rb +53 -40
- data/redis-em-mutex.gemspec +2 -2
- data/spec/redis-em-mutex-condition.rb +6 -1
- data/spec/redis-em-mutex-features.rb +48 -8
- data/spec/redis-em-mutex-semaphores.rb +13 -2
- metadata +48 -18
data/HISTORY.rdoc
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
0.2.2
|
2
|
+
- fixed: uuid must be set only once
|
3
|
+
- fixed: forking process or setup before reactor is running would hang
|
4
|
+
current fiber forever on first failed lock
|
5
|
+
- added: new setup option :redis_factory
|
6
|
+
- fixed: doc format errors
|
7
|
+
|
1
8
|
0.2.1
|
2
9
|
- fixed: sleep
|
3
10
|
- fixed: possible deadlock after #unlock! with multiple names
|
data/{README.rdoc → README.md}
RENAMED
@@ -1,14 +1,17 @@
|
|
1
|
-
|
1
|
+
redis-em-mutex
|
2
|
+
==============
|
2
3
|
|
3
|
-
Author
|
4
|
+
Author: Rafał Michalski (mailto:rafal@yeondir.com)
|
4
5
|
|
5
6
|
* http://github.com/royaltm/redis-em-mutex
|
6
7
|
|
7
|
-
|
8
|
+
DESCRIPTION
|
9
|
+
-----------
|
8
10
|
|
9
|
-
|
11
|
+
__redis-em-mutex__ is the cross server-process-fiber EventMachine + Redis based semaphore.
|
10
12
|
|
11
|
-
|
13
|
+
FEATURES
|
14
|
+
--------
|
12
15
|
|
13
16
|
* only for EventMachine
|
14
17
|
* no CPU-intensive sleep/polling while waiting for lock to become available
|
@@ -24,78 +27,90 @@ Author:: Rafał Michalski (mailto:rafal@yeondir.com)
|
|
24
27
|
* compatible with Synchrony::Thread::ConditionVariable
|
25
28
|
* extendable (beyond fibers) mutex ownership
|
26
29
|
|
27
|
-
|
30
|
+
BUGS/LIMITATIONS
|
31
|
+
----------------
|
28
32
|
|
29
33
|
* only for EventMachine
|
30
34
|
* NOT thread-safe
|
31
35
|
* locking order between concurrent processes is undetermined (no FIFO)
|
32
36
|
* it's not nifty, rather somewhat complicated
|
33
37
|
|
34
|
-
|
38
|
+
REQUIREMENTS
|
39
|
+
------------
|
35
40
|
|
36
|
-
* ruby >= 1.9 (tested: 1.9.3-p194, 1.9.2-p320, 1.9.1-p378)
|
41
|
+
* ruby >= 1.9 (tested: ruby 1.9.3p374, 1.9.3-p194, 1.9.2-p320, 1.9.1-p378)
|
37
42
|
* http://github.com/redis/redis-rb ~> 3.0.1
|
38
43
|
* http://rubyeventmachine.com ~> 1.0.0
|
39
44
|
* (optional) http://github.com/igrigorik/em-synchrony
|
40
45
|
|
41
|
-
|
46
|
+
INSTALL
|
47
|
+
-------
|
42
48
|
|
43
|
-
|
49
|
+
```
|
50
|
+
$ [sudo] gem install redis-em-mutex
|
51
|
+
```
|
44
52
|
|
45
|
-
|
53
|
+
#### Gemfile
|
46
54
|
|
47
|
-
|
55
|
+
```ruby
|
56
|
+
gem "redis-em-mutex", "~> 0.2.2"
|
57
|
+
```
|
48
58
|
|
49
|
-
|
59
|
+
#### Github
|
50
60
|
|
51
|
-
|
61
|
+
```
|
62
|
+
git clone git://github.com/royaltm/redis-em-mutex.git
|
63
|
+
```
|
52
64
|
|
53
|
-
|
65
|
+
USAGE
|
66
|
+
-----
|
54
67
|
|
55
|
-
|
56
|
-
|
68
|
+
```ruby
|
69
|
+
require 'em-synchrony'
|
70
|
+
require 'redis-em-mutex'
|
57
71
|
|
58
|
-
|
72
|
+
Redis::EM::Mutex.setup(size: 10, url: 'redis:///1', expire: 600)
|
59
73
|
|
60
|
-
|
74
|
+
# or
|
61
75
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
76
|
+
Redis::EM::Mutex.setup do |opts|
|
77
|
+
opts.size = 10
|
78
|
+
opts.url = 'redis:///1'
|
79
|
+
end
|
67
80
|
|
68
81
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
82
|
+
EM.synchrony do
|
83
|
+
Redis::EM::Mutex.synchronize('resource.lock') do
|
84
|
+
# ... do something with resource
|
85
|
+
end
|
73
86
|
|
74
|
-
|
87
|
+
# or
|
75
88
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
89
|
+
mutex = Redis::EM::Mutex.new('resource.lock')
|
90
|
+
mutex.synchronize do
|
91
|
+
# ... do something with resource
|
92
|
+
end
|
80
93
|
|
81
|
-
|
94
|
+
# or
|
82
95
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
96
|
+
begin
|
97
|
+
mutex.lock
|
98
|
+
# ... do something with resource
|
99
|
+
ensure
|
100
|
+
mutex.unlock
|
101
|
+
end
|
89
102
|
|
90
|
-
|
103
|
+
# ...
|
91
104
|
|
92
|
-
|
93
|
-
|
94
|
-
|
105
|
+
Redis::EM::Mutex.stop_watcher
|
106
|
+
EM.stop
|
107
|
+
end
|
108
|
+
```
|
95
109
|
|
96
|
-
|
110
|
+
### Namespaces
|
97
111
|
|
98
|
-
|
112
|
+
```ruby
|
113
|
+
Redis::EM::Mutex.setup(ns: 'my_namespace')
|
99
114
|
|
100
115
|
# or multiple namespaces:
|
101
116
|
|
@@ -103,35 +118,41 @@ Author:: Rafał Michalski (mailto:rafal@yeondir.com)
|
|
103
118
|
|
104
119
|
EM.synchrony do
|
105
120
|
ns.synchronize('foo') do
|
106
|
-
.... do something with foo
|
121
|
+
# .... do something with foo
|
107
122
|
end
|
108
|
-
|
123
|
+
|
124
|
+
# ...
|
109
125
|
EM.stop
|
110
126
|
end
|
127
|
+
```
|
111
128
|
|
112
|
-
|
129
|
+
### Multi-locks
|
113
130
|
|
131
|
+
```ruby
|
114
132
|
EM.synchrony do
|
115
133
|
Redis::EM::Mutex.synchronize('foo', 'bar', 'baz') do
|
116
|
-
.... do something with foo, bar and baz
|
134
|
+
# .... do something with foo, bar and baz
|
117
135
|
end
|
118
|
-
|
136
|
+
|
137
|
+
# ...
|
119
138
|
EM.stop
|
120
139
|
end
|
140
|
+
```
|
121
141
|
|
122
|
-
|
142
|
+
### Locking options
|
123
143
|
|
144
|
+
```ruby
|
124
145
|
EM.synchrony do
|
125
146
|
begin
|
126
147
|
Redis::EM::Mutex.synchronize('foo', 'bar', block: 0.25) do
|
127
|
-
.... do something with foo and bar
|
148
|
+
# .... do something with foo and bar
|
128
149
|
end
|
129
150
|
rescue Redis::EM::Mutex::MutexTimeout
|
130
|
-
... locking timed out
|
151
|
+
# ... locking timed out
|
131
152
|
end
|
132
153
|
|
133
154
|
Redis::EM::Mutex.synchronize('foo', 'bar', expire: 60) do |mutex|
|
134
|
-
.... do something with foo and bar in less than 60 seconds
|
155
|
+
# .... do something with foo and bar in less than 60 seconds
|
135
156
|
if mutex.refresh(120)
|
136
157
|
# now we have additional 120 seconds until lock expires
|
137
158
|
else
|
@@ -139,23 +160,26 @@ Author:: Rafał Michalski (mailto:rafal@yeondir.com)
|
|
139
160
|
end
|
140
161
|
end
|
141
162
|
|
142
|
-
...
|
163
|
+
# ...
|
143
164
|
EM.stop
|
144
165
|
end
|
166
|
+
```
|
145
167
|
|
146
|
-
|
168
|
+
### Macro-style definition
|
147
169
|
|
148
170
|
Borrowed from http://github.com/kenn/redis-mutex.
|
149
171
|
Redis::EM::Mutex::Macro is a mixin which protects selected instance methods of a class with a mutex.
|
150
172
|
The locking scope will be Mutex global namespace + class name + method name.
|
151
173
|
|
174
|
+
```ruby
|
152
175
|
class TheClass
|
153
176
|
include Redis::EM::Mutex::Macro
|
154
177
|
|
155
178
|
auto_mutex
|
156
179
|
def critical_run
|
157
|
-
... do some critical stuff
|
158
|
-
....only one fiber in one process on one machine is executing
|
180
|
+
# ... do some critical stuff
|
181
|
+
# ....only one fiber in one process on one machine is executing
|
182
|
+
# this instance method of any instance of defined class
|
159
183
|
end
|
160
184
|
|
161
185
|
auto_mutex expire: 100, ns: '**TheClass**'
|
@@ -163,39 +187,41 @@ The locking scope will be Mutex global namespace + class name + method name.
|
|
163
187
|
|
164
188
|
auto_mutex # start and stop will be protected
|
165
189
|
def start
|
166
|
-
...
|
190
|
+
# ...
|
167
191
|
end
|
168
192
|
|
169
193
|
def stop
|
170
|
-
...
|
194
|
+
# ...
|
171
195
|
end
|
172
196
|
|
173
197
|
no_auto_mutex
|
174
198
|
def some_unprotected_method
|
175
|
-
...
|
199
|
+
# ...
|
176
200
|
end
|
177
201
|
|
178
202
|
auto_mutex :run_long, expire: 100000, block: 10, on_timeout: :cant_run
|
179
203
|
def run_long
|
180
|
-
...
|
204
|
+
# ...
|
181
205
|
end
|
182
206
|
|
183
207
|
def cant_run
|
184
|
-
...
|
208
|
+
# ...
|
185
209
|
end
|
186
210
|
|
187
211
|
def foo
|
188
|
-
...
|
212
|
+
# ...
|
189
213
|
end
|
190
214
|
auto_mutex :foo, block: 0, on_timeout: proc { puts 'bar!' }
|
191
215
|
|
192
216
|
end
|
217
|
+
```
|
193
218
|
|
194
|
-
|
219
|
+
### ConditionVariable
|
195
220
|
|
196
221
|
Redis::EM::Mutex may be used with EventMachine::Synchrony::Thread::ConditionVariable
|
197
222
|
in place of EventMachine::Synchrony::Thread::Mutex.
|
198
223
|
|
224
|
+
```ruby
|
199
225
|
mutex = Redis::EM::Mutex.new('resource')
|
200
226
|
resource = EM::Synchrony::Thread::ConditionVariable.new
|
201
227
|
EM::Synchrony.next_tick do
|
@@ -211,10 +237,11 @@ in place of EventMachine::Synchrony::Thread::Mutex.
|
|
211
237
|
resource.signal
|
212
238
|
}
|
213
239
|
end
|
240
|
+
```
|
214
241
|
|
215
|
-
|
242
|
+
### Advanced
|
216
243
|
|
217
|
-
|
244
|
+
#### Customized owner
|
218
245
|
|
219
246
|
In some cases you might want to extend the ownership of a locked semaphore beyond the current fiber.
|
220
247
|
That is to be able to "share" a locked semaphore between group of fibers associated with some external resource.
|
@@ -234,6 +261,7 @@ This way we make sure that any other connection from the same ip address made to
|
|
234
261
|
will have different ownership defined. At the same time all the fibers working with that Connection instance
|
235
262
|
can access mutex's #refresh and #unlock methods concurrently.
|
236
263
|
|
264
|
+
```ruby
|
237
265
|
module MutexedConnection
|
238
266
|
def post_init
|
239
267
|
@buffer = ""
|
@@ -274,13 +302,16 @@ can access mutex's #refresh and #unlock methods concurrently.
|
|
274
302
|
Redis::EM::Mutex.setup(size: 10, url: 'redis:///1', expire: 600)
|
275
303
|
EM.start_server "0.0.0.0", 6969, MutexedConnection
|
276
304
|
}
|
305
|
+
```
|
277
306
|
|
278
|
-
|
307
|
+
#### Forking live
|
279
308
|
|
280
309
|
You may safely fork process while running event reactor and having locked semaphores.
|
281
310
|
The locked semaphores in newly forked process will become unlocked while
|
282
311
|
their locked status in parent process will be preserved.
|
283
312
|
|
313
|
+
|
314
|
+
```ruby
|
284
315
|
mutex = Redis::EM::Mutex.new('resource1', 'resource2', expire: 60)
|
285
316
|
|
286
317
|
EM.synchrony do
|
@@ -294,9 +325,10 @@ their locked status in parent process will be preserved.
|
|
294
325
|
mutex.locked? # true
|
295
326
|
mutex.owned? # true
|
296
327
|
|
297
|
-
|
328
|
+
# ...
|
298
329
|
end
|
299
|
-
|
330
|
+
|
331
|
+
# ...
|
300
332
|
Redis::EM::Mutex.stop_watcher
|
301
333
|
EM.stop
|
302
334
|
end.resume
|
@@ -308,11 +340,33 @@ their locked status in parent process will be preserved.
|
|
308
340
|
mutex.unlock
|
309
341
|
mutex.owned? # false
|
310
342
|
|
311
|
-
...
|
343
|
+
# ...
|
312
344
|
Redis::EM::Mutex.stop_watcher
|
313
345
|
EM.stop
|
314
346
|
end
|
347
|
+
```
|
348
|
+
|
349
|
+
#### Redis factory
|
350
|
+
|
351
|
+
Want to use some non-standard redis options or customized client for semaphore watcher and/or redis pool?
|
352
|
+
Use `:redis_factory` option then.
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
require 'redis-sentinel'
|
356
|
+
|
357
|
+
Redis::EM::Mutex.setup do |opts|
|
358
|
+
opts.size = 10
|
359
|
+
opts.password = 'password'
|
360
|
+
opts.db = 11
|
361
|
+
opts.redis_factory = proc do |options|
|
362
|
+
Redis.new options.merge(
|
363
|
+
master_name: "my_master",
|
364
|
+
sentinels: [{host: "redis1", port: 6379}, {host: "redis2", port: 6380}])
|
365
|
+
end
|
366
|
+
end
|
367
|
+
```
|
315
368
|
|
316
|
-
|
369
|
+
LICENCE
|
370
|
+
-------
|
317
371
|
|
318
372
|
The MIT License - Copyright (c) 2012 Rafał Michalski
|
data/Rakefile
CHANGED
data/lib/redis/em-mutex.rb
CHANGED
@@ -32,19 +32,20 @@ class Redis
|
|
32
32
|
include Errors
|
33
33
|
extend Errors
|
34
34
|
|
35
|
-
@@connection_pool_class = nil
|
36
|
-
@@connection_retry_max = 10
|
37
|
-
@@default_expire = 3600*24
|
38
|
-
AUTO_NAME_SEED = '__@'
|
39
35
|
SIGNAL_QUEUE_CHANNEL = "::#{self.name}::"
|
36
|
+
AUTO_NAME_SEED = '__@'
|
37
|
+
DEFAULT_RECONNECT_MAX_RETRIES = 10
|
38
|
+
@@default_expire = 3600*24
|
40
39
|
@@name_index = AUTO_NAME_SEED
|
41
40
|
@@redis_pool = nil
|
42
|
-
@@redis_watcher = nil
|
43
41
|
@@watching = false
|
44
|
-
@@watcher_subscribed = false
|
45
42
|
@@signal_queue = Hash.new {|h,k| h[k] = []}
|
46
43
|
@@ns = nil
|
47
|
-
@@uuid
|
44
|
+
@@uuid ||= if SecureRandom.respond_to?(:uuid)
|
45
|
+
SecureRandom.uuid
|
46
|
+
else
|
47
|
+
SecureRandom.base64(24)
|
48
|
+
end
|
48
49
|
|
49
50
|
attr_reader :names, :ns, :block_timeout
|
50
51
|
alias_method :namespace, :ns
|
@@ -264,10 +265,10 @@ class Redis
|
|
264
265
|
::EM.next_tick { fiber.resume if fiber } if fiber
|
265
266
|
end
|
266
267
|
begin
|
268
|
+
Mutex.start_watcher unless watching?
|
267
269
|
queues = names.map {|n| @@signal_queue[n] << handler }
|
268
270
|
ident_match = owner_ident
|
269
271
|
until try_lock
|
270
|
-
Mutex.start_watcher unless watching?
|
271
272
|
start_time = Time.now.to_f
|
272
273
|
expire_time = nil
|
273
274
|
@@redis_pool.execute(false) do |r|
|
@@ -382,10 +383,17 @@ class Redis
|
|
382
383
|
def self.watching?; @@watching == $$; end
|
383
384
|
|
384
385
|
class << self
|
386
|
+
attr_reader :reconnect_max_retries
|
387
|
+
def reconnect_forever?
|
388
|
+
@reconnect_max_retries < 0
|
389
|
+
end
|
390
|
+
def reconnect_max_retries=(max)
|
391
|
+
@reconnect_max_retries = max == :forever ? -1 : max.to_i
|
392
|
+
end
|
385
393
|
def ns; @@ns; end
|
386
394
|
def ns=(namespace); @@ns = namespace; end
|
387
395
|
alias_method :namespace, :ns
|
388
|
-
alias_method :
|
396
|
+
alias_method :namespace=, :ns=
|
389
397
|
|
390
398
|
# Default value of expiration timeout in seconds.
|
391
399
|
def default_expire; @@default_expire; end
|
@@ -405,18 +413,19 @@ class Redis
|
|
405
413
|
# global options:
|
406
414
|
#
|
407
415
|
# - :connection_pool_class - default is ::EM::Synchrony::ConnectionPool
|
416
|
+
# - :redis_factory - default is proc {|opts| Redis.new opts }
|
408
417
|
# - :expire - sets global Mutex.default_expire
|
409
418
|
# - :ns - sets global Mutex.namespace
|
410
419
|
# - :reconnect_max - maximum num. of attempts to re-establish
|
411
420
|
# connection to redis server;
|
412
421
|
# default is 10; set to 0 to disable re-connecting;
|
413
|
-
# set to -1 to attempt forever
|
422
|
+
# set to -1 or :forever to attempt forever
|
414
423
|
#
|
415
424
|
# redis connection options:
|
416
425
|
#
|
417
426
|
# - :size - redis connection pool size
|
418
427
|
#
|
419
|
-
# passed directly to
|
428
|
+
# passed directly to redis_factory:
|
420
429
|
#
|
421
430
|
# - :url - redis server url
|
422
431
|
#
|
@@ -434,10 +443,10 @@ class Redis
|
|
434
443
|
# - :redis - initialized ConnectionPool of Redis clients.
|
435
444
|
def setup(opts = {})
|
436
445
|
stop_watcher
|
446
|
+
@watcher_subscribed = nil
|
437
447
|
opts = OpenStruct.new(opts)
|
438
448
|
yield opts if block_given?
|
439
|
-
|
440
|
-
@redis_options = redis_options = {:driver => :synchrony}
|
449
|
+
redis_options = {:driver => :synchrony}
|
441
450
|
redis_updater = proc do |redis|
|
442
451
|
redis_options.update({
|
443
452
|
:scheme => redis.scheme,
|
@@ -454,33 +463,37 @@ class Redis
|
|
454
463
|
redis_options[:url] = opts.url
|
455
464
|
end
|
456
465
|
redis_updater.call opts
|
457
|
-
namespace = opts.ns
|
458
466
|
pool_size = (opts.size.to_i.nonzero? || 1).abs
|
459
467
|
self.default_expire = opts.expire if opts.expire
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
468
|
+
self.reconnect_max_retries = opts.reconnect_max if opts.reconnect_max
|
469
|
+
@connection_pool_class = opts.connection_pool_class if opts.connection_pool_class.kind_of?(Class)
|
470
|
+
@redis_options = redis_options
|
471
|
+
@reconnect_max_retries ||= DEFAULT_RECONNECT_MAX_RETRIES
|
472
|
+
@redis_factory = opts.redis_factory if opts.redis_factory
|
473
|
+
@redis_factory ||= proc {|opts| Redis.new opts }
|
474
|
+
raise TypeError, "redis_factory should respond to [] method" unless @redis_factory.respond_to?(:[])
|
475
|
+
@@ns = opts.ns if opts.ns
|
467
476
|
unless (@@redis_pool = redis)
|
468
|
-
unless
|
477
|
+
unless @connection_pool_class
|
469
478
|
begin
|
470
479
|
require 'em-synchrony/connection_pool' unless defined?(::EM::Synchrony::ConnectionPool)
|
471
480
|
rescue LoadError
|
472
481
|
raise ":connection_pool_class required; could not fall back to EM::Synchrony::ConnectionPool - gem install em-synchrony"
|
473
482
|
end
|
474
|
-
|
483
|
+
@connection_pool_class = ::EM::Synchrony::ConnectionPool
|
475
484
|
end
|
476
|
-
@@redis_pool =
|
477
|
-
|
485
|
+
@@redis_pool = @connection_pool_class.new(size: pool_size) do
|
486
|
+
@redis_factory[redis_options]
|
478
487
|
end
|
479
488
|
end
|
480
|
-
|
489
|
+
@redis_watcher = @redis_factory[redis_options]
|
481
490
|
start_watcher if ::EM.reactor_running?
|
482
491
|
end
|
483
492
|
|
493
|
+
def ready?
|
494
|
+
!!@@redis_pool
|
495
|
+
end
|
496
|
+
|
484
497
|
# resets Mutex's automatic name generator
|
485
498
|
def reset_autoname
|
486
499
|
@@name_index = AUTO_NAME_SEED
|
@@ -497,20 +510,20 @@ class Redis
|
|
497
510
|
# If EventMachine is to be re-started (or after EM.fork_reactor) this method may be used instead of
|
498
511
|
# Mutex.setup for "lightweight" startup procedure.
|
499
512
|
def start_watcher
|
500
|
-
raise MutexError, "call #{self.class}::setup first" unless
|
513
|
+
raise MutexError, "call #{self.class}::setup first" unless @redis_watcher
|
501
514
|
return if watching?
|
502
515
|
if @@watching # Process id changed, we've been forked alive!
|
503
|
-
|
516
|
+
@redis_watcher = @redis_factory[@redis_options]
|
504
517
|
@@signal_queue.clear
|
505
518
|
end
|
506
519
|
@@watching = $$
|
507
520
|
retries = 0
|
508
521
|
Fiber.new do
|
509
522
|
begin
|
510
|
-
|
523
|
+
@redis_watcher.subscribe(SIGNAL_QUEUE_CHANNEL) do |on|
|
511
524
|
on.subscribe do |channel,|
|
512
525
|
if channel == SIGNAL_QUEUE_CHANNEL
|
513
|
-
|
526
|
+
@watcher_subscribed = true
|
514
527
|
retries = 0
|
515
528
|
wakeup_queue_all
|
516
529
|
end
|
@@ -527,22 +540,22 @@ class Redis
|
|
527
540
|
end
|
528
541
|
end
|
529
542
|
on.unsubscribe do |channel,|
|
530
|
-
|
543
|
+
@watcher_subscribed = false if channel == SIGNAL_QUEUE_CHANNEL
|
531
544
|
end
|
532
545
|
end
|
533
546
|
break
|
534
547
|
rescue Redis::BaseConnectionError, EventMachine::ConnectionError => e
|
535
|
-
|
548
|
+
@watcher_subscribed = false
|
536
549
|
warn e.message
|
537
550
|
retries+= 1
|
538
|
-
if retries >
|
551
|
+
if retries > reconnect_max_retries && reconnect_max_retries >= 0
|
539
552
|
@@watching = false
|
540
553
|
else
|
541
554
|
sleep retries > 1 ? 1 : 0.1
|
542
555
|
end
|
543
556
|
end while watching?
|
544
557
|
end.resume
|
545
|
-
until
|
558
|
+
until @watcher_subscribed
|
546
559
|
raise MutexError, "Can not establish watcher channel connection!" unless watching?
|
547
560
|
fiber = Fiber.current
|
548
561
|
::EM.next_tick { fiber.resume }
|
@@ -558,22 +571,22 @@ class Redis
|
|
558
571
|
end
|
559
572
|
|
560
573
|
# Stops the watcher of the "unlock" channel.
|
561
|
-
# It should be called before
|
574
|
+
# It should be called before stopping EvenMachine otherwise
|
562
575
|
# EM might wait forever for channel connection to be closed.
|
563
576
|
#
|
564
|
-
# Raises MutexError if there are still some fibers waiting for
|
577
|
+
# Raises MutexError if there are still some fibers waiting for lock.
|
565
578
|
# Pass `true` to forcefully stop it. This might instead cause
|
566
579
|
# MutexError to be raised in waiting fibers.
|
567
580
|
def stop_watcher(force = false)
|
568
581
|
return unless watching?
|
569
|
-
raise MutexError, "call #{self.class}::setup first" unless
|
582
|
+
raise MutexError, "call #{self.class}::setup first" unless @redis_watcher
|
570
583
|
unless @@signal_queue.empty? || force
|
571
584
|
raise MutexError, "can't stop: active signal queue handlers"
|
572
585
|
end
|
573
586
|
@@watching = false
|
574
|
-
if
|
575
|
-
|
576
|
-
while
|
587
|
+
if @watcher_subscribed
|
588
|
+
@redis_watcher.unsubscribe SIGNAL_QUEUE_CHANNEL
|
589
|
+
while @watcher_subscribed
|
577
590
|
fiber = Fiber.current
|
578
591
|
::EM.next_tick { fiber.resume }
|
579
592
|
Fiber.yield
|
data/redis-em-mutex.gemspec
CHANGED
@@ -15,9 +15,9 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.files = `git ls-files`.split("\n") - ['.gitignore']
|
16
16
|
s.test_files = Dir.glob("spec/**/*")
|
17
17
|
s.rdoc_options << "--title" << "redis-em-mutex" <<
|
18
|
-
"--main" << "README.
|
18
|
+
"--main" << "README.md"
|
19
19
|
s.has_rdoc = true
|
20
|
-
s.extra_rdoc_files = ["README.
|
20
|
+
s.extra_rdoc_files = ["README.md"]
|
21
21
|
s.requirements << "Redis server 2.4+"
|
22
22
|
s.add_runtime_dependency "redis", ">= 3.0.1"
|
23
23
|
s.add_runtime_dependency "hiredis", "~> 0.4.5"
|
@@ -106,14 +106,19 @@ describe Redis::EM::Mutex do
|
|
106
106
|
mutex = described_class.new(*@lock_names)
|
107
107
|
resource = ::EM::Synchrony::Thread::ConditionVariable.new
|
108
108
|
signal = nil
|
109
|
+
delta = nil
|
109
110
|
fiber = Fiber.current
|
110
111
|
::EM::Synchrony.next_tick do
|
111
112
|
mutex.synchronize {
|
113
|
+
signal = true
|
112
114
|
resource.wait(mutex)
|
113
115
|
fiber.resume Time.now
|
114
116
|
}
|
115
117
|
end
|
116
118
|
::EM::Synchrony.next_tick do
|
119
|
+
t1 = Time.now
|
120
|
+
::EM::Synchrony.sleep(0.01) while signal.nil?
|
121
|
+
delta = Time.now - t1
|
117
122
|
mutex.synchronize {
|
118
123
|
::EM::Synchrony.sleep(0.2)
|
119
124
|
resource.signal
|
@@ -123,7 +128,7 @@ describe Redis::EM::Mutex do
|
|
123
128
|
start = Time.now
|
124
129
|
now = Fiber.yield
|
125
130
|
(now - signal).should be_within(0.002).of(0.001)
|
126
|
-
(now - start).should be_within(0.01).of(0.2)
|
131
|
+
(now - start).should be_within(0.01).of(0.2 + delta)
|
127
132
|
mutex.synchronize do
|
128
133
|
signal = nil
|
129
134
|
end
|
@@ -3,6 +3,8 @@ require 'em-synchrony'
|
|
3
3
|
require 'em-synchrony/connection_pool'
|
4
4
|
require 'redis-em-mutex'
|
5
5
|
|
6
|
+
class TestDummyConnectionPool < EM::Synchrony::ConnectionPool; end
|
7
|
+
|
6
8
|
describe Redis::EM::Mutex do
|
7
9
|
|
8
10
|
it "should raise MutexError while redis server not found on setup" do
|
@@ -22,7 +24,7 @@ describe Redis::EM::Mutex do
|
|
22
24
|
it "should setup with redis connection pool" do
|
23
25
|
described_class.setup(redis: @redis_pool)
|
24
26
|
described_class.class_variable_get(:'@@redis_pool').should be @redis_pool
|
25
|
-
redis = described_class.
|
27
|
+
redis = described_class.instance_variable_get(:'@redis_watcher')
|
26
28
|
described_class.stop_watcher
|
27
29
|
redis.should be_an_instance_of Redis
|
28
30
|
redis.client.host.should eq 'localhost'
|
@@ -32,15 +34,18 @@ describe Redis::EM::Mutex do
|
|
32
34
|
|
33
35
|
it "should setup with various options" do
|
34
36
|
described_class.setup do |cfg|
|
35
|
-
cfg.expire = 42
|
36
|
-
cfg.ns = 'redis rulez!'
|
37
|
-
cfg.reconnect_max =
|
37
|
+
cfg.expire = 42 # - sets global Mutex.default_expire
|
38
|
+
cfg.ns = 'redis rulez!' # - sets global Mutex.namespace
|
39
|
+
cfg.reconnect_max = :forever # - maximum num. of attempts to re-establish
|
38
40
|
cfg.url = 'redis://127.0.0.1/2'
|
39
41
|
cfg.size = 10
|
40
42
|
end
|
41
43
|
described_class.namespace.should eq 'redis rulez!'
|
42
44
|
described_class.default_expire.should eq 42
|
43
|
-
described_class.
|
45
|
+
described_class.reconnect_forever?.should be true
|
46
|
+
described_class.instance_variable_get(:'@reconnect_max_retries').should eq -1
|
47
|
+
described_class.reconnect_max_retries = 0
|
48
|
+
described_class.reconnect_forever?.should be false
|
44
49
|
redis_pool = described_class.class_variable_get(:'@@redis_pool')
|
45
50
|
redis_pool.should be_an_instance_of EM::Synchrony::ConnectionPool
|
46
51
|
redis_pool.should_not be @redis_pool
|
@@ -48,7 +53,7 @@ describe Redis::EM::Mutex do
|
|
48
53
|
redis_pool.client.db.should eq 2
|
49
54
|
redis_pool.client.port.should eq 6379
|
50
55
|
redis_pool.client.scheme.should eq 'redis'
|
51
|
-
redis = described_class.
|
56
|
+
redis = described_class.instance_variable_get(:'@redis_watcher')
|
52
57
|
described_class.stop_watcher
|
53
58
|
redis.should be_an_instance_of Redis
|
54
59
|
redis.client.host.should eq '127.0.0.1'
|
@@ -63,15 +68,16 @@ describe Redis::EM::Mutex do
|
|
63
68
|
cfg.host = 'localhost'
|
64
69
|
cfg.port = 6379
|
65
70
|
cfg.db = 3
|
71
|
+
cfg.connection_pool_class = TestDummyConnectionPool
|
66
72
|
end
|
67
73
|
redis_pool = described_class.class_variable_get(:'@@redis_pool')
|
68
|
-
redis_pool.should be_an_instance_of
|
74
|
+
redis_pool.should be_an_instance_of TestDummyConnectionPool
|
69
75
|
redis_pool.should_not be @redis_pool
|
70
76
|
redis_pool.client.host.should eq 'localhost'
|
71
77
|
redis_pool.client.db.should eq 3
|
72
78
|
redis_pool.client.port.should eq 6379
|
73
79
|
redis_pool.client.scheme.should eq 'redis'
|
74
|
-
redis = described_class.
|
80
|
+
redis = described_class.instance_variable_get(:'@redis_watcher')
|
75
81
|
redis.should be_an_instance_of Redis
|
76
82
|
described_class.stop_watcher
|
77
83
|
redis.client.host.should eq 'localhost'
|
@@ -80,17 +86,50 @@ describe Redis::EM::Mutex do
|
|
80
86
|
redis.client.scheme.should eq 'redis'
|
81
87
|
end
|
82
88
|
|
89
|
+
it "should be able to setup with redis factory" do
|
90
|
+
counter = 0
|
91
|
+
described_class.setup do |cfg|
|
92
|
+
cfg.redis = @redis_pool
|
93
|
+
cfg.redis_factory = proc do |opts|
|
94
|
+
counter += 1
|
95
|
+
Redis.new opts
|
96
|
+
end
|
97
|
+
end
|
98
|
+
counter.should eq 1
|
99
|
+
counter = 0
|
100
|
+
described_class.setup do |cfg|
|
101
|
+
cfg.size = 5
|
102
|
+
cfg.redis_factory = proc do |opts|
|
103
|
+
counter += 1
|
104
|
+
Redis.new opts
|
105
|
+
end
|
106
|
+
end
|
107
|
+
counter.should eq 6
|
108
|
+
end
|
109
|
+
|
110
|
+
|
83
111
|
it "should be able to sleep" do
|
84
112
|
t = Time.now
|
85
113
|
described_class.sleep 0.11
|
86
114
|
(Time.now - t).should be_within(0.02).of(0.11)
|
87
115
|
end
|
88
116
|
|
117
|
+
it "should not change uuid" do
|
118
|
+
@uuid.should be_an_instance_of String
|
119
|
+
described_class.class_variable_get(:@@uuid).should eq @uuid
|
120
|
+
end
|
121
|
+
|
89
122
|
around(:each) do |testcase|
|
90
123
|
@after_em_stop = nil
|
124
|
+
@exception = nil
|
91
125
|
::EM.synchrony do
|
92
126
|
begin
|
93
127
|
testcase.call
|
128
|
+
raise @exception if @exception
|
129
|
+
described_class.stop_watcher
|
130
|
+
rescue => e
|
131
|
+
described_class.stop_watcher(true)
|
132
|
+
raise e
|
94
133
|
ensure
|
95
134
|
::EM.stop
|
96
135
|
end
|
@@ -100,5 +139,6 @@ describe Redis::EM::Mutex do
|
|
100
139
|
|
101
140
|
before(:all) do
|
102
141
|
@redis_pool = EM::Synchrony::ConnectionPool.new(size: 10) { Redis.new url: 'redis://localhost/1' }
|
142
|
+
@uuid = described_class.class_variable_get(:@@uuid)
|
103
143
|
end
|
104
144
|
end
|
@@ -6,6 +6,10 @@ require 'redis-em-mutex'
|
|
6
6
|
|
7
7
|
describe Redis::EM::Mutex do
|
8
8
|
|
9
|
+
it "should be ready" do
|
10
|
+
described_class.ready?.should be true
|
11
|
+
end
|
12
|
+
|
9
13
|
it "should lock and prevent locking on the same semaphore" do
|
10
14
|
described_class.new(@lock_names.first).owned?.should be false
|
11
15
|
mutex = described_class.lock(@lock_names.first)
|
@@ -83,6 +87,7 @@ describe Redis::EM::Mutex do
|
|
83
87
|
mutex.should be_an_instance_of described_class
|
84
88
|
mutex.owned?.should be true
|
85
89
|
locked = true
|
90
|
+
start = nil
|
86
91
|
::EM::Synchrony.next_tick do
|
87
92
|
begin
|
88
93
|
locked.should be true
|
@@ -96,7 +101,6 @@ describe Redis::EM::Mutex do
|
|
96
101
|
end
|
97
102
|
mutex.owned?.should be false
|
98
103
|
::EM::Synchrony.sleep 0.1
|
99
|
-
start = Time.now
|
100
104
|
::EM::Synchrony::FiberIterator.new(@lock_names, @lock_names.length).each do |name|
|
101
105
|
begin
|
102
106
|
locked.should be true
|
@@ -122,6 +126,7 @@ describe Redis::EM::Mutex do
|
|
122
126
|
|
123
127
|
locked = true
|
124
128
|
mutex.lock.should be true
|
129
|
+
start = Time.now
|
125
130
|
::EM::Synchrony.sleep 0.25
|
126
131
|
locked = 10
|
127
132
|
mutex.unlock.should be_an_instance_of described_class
|
@@ -283,19 +288,24 @@ describe Redis::EM::Mutex do
|
|
283
288
|
end
|
284
289
|
|
285
290
|
it "should lock and the other process should acquire lock as soon as possible" do
|
291
|
+
described_class.watching?.should be false
|
286
292
|
mutex = described_class.lock(*@lock_names)
|
293
|
+
described_class.watching?.should be true
|
287
294
|
mutex.should be_an_instance_of described_class
|
288
295
|
time_key1 = SecureRandom.random_bytes
|
289
296
|
time_key2 = SecureRandom.random_bytes
|
290
297
|
::EM.fork_reactor do
|
298
|
+
described_class.watching?.should be false
|
291
299
|
Fiber.new do
|
292
300
|
begin
|
293
301
|
redis = Redis.new @redis_options
|
294
302
|
redis.set time_key1, Time.now.to_f.to_s
|
295
303
|
mutex.synchronize do
|
304
|
+
described_class.watching?.should be true
|
296
305
|
redis.set time_key2, Time.now.to_f.to_s
|
297
306
|
end
|
298
307
|
described_class.stop_watcher(false)
|
308
|
+
described_class.watching?.should be false
|
299
309
|
# rescue => e
|
300
310
|
# warn e.inspect
|
301
311
|
ensure
|
@@ -303,7 +313,9 @@ describe Redis::EM::Mutex do
|
|
303
313
|
end
|
304
314
|
end.resume
|
305
315
|
end
|
316
|
+
described_class.watching?.should be true
|
306
317
|
EM::Synchrony.sleep 0.25
|
318
|
+
described_class.watching?.should be true
|
307
319
|
mutex.owned?.should be true
|
308
320
|
mutex.unlock.should be_an_instance_of described_class
|
309
321
|
time = Time.now.to_f
|
@@ -314,7 +326,6 @@ describe Redis::EM::Mutex do
|
|
314
326
|
t1.should be_an_instance_of String
|
315
327
|
t1.to_f.should be < time - 0.25
|
316
328
|
t2.should be_an_instance_of String
|
317
|
-
t2.to_f.should be > time
|
318
329
|
t2.to_f.should be_within(0.001).of(time)
|
319
330
|
redis.del(time_key1, time_key2)
|
320
331
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-em-mutex
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: 3.0.1
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.1
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: hiredis
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ~>
|
@@ -32,10 +37,15 @@ dependencies:
|
|
32
37
|
version: 0.4.5
|
33
38
|
type: :runtime
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.4.5
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: eventmachine
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ! '>='
|
@@ -43,10 +53,15 @@ dependencies:
|
|
43
53
|
version: 1.0.0.beta.1
|
44
54
|
type: :runtime
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.0.beta.1
|
47
62
|
- !ruby/object:Gem::Dependency
|
48
63
|
name: rspec
|
49
|
-
requirement:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
50
65
|
none: false
|
51
66
|
requirements:
|
52
67
|
- - ~>
|
@@ -54,10 +69,15 @@ dependencies:
|
|
54
69
|
version: 2.8.0
|
55
70
|
type: :development
|
56
71
|
prerelease: false
|
57
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.8.0
|
58
78
|
- !ruby/object:Gem::Dependency
|
59
79
|
name: eventmachine
|
60
|
-
requirement:
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
61
81
|
none: false
|
62
82
|
requirements:
|
63
83
|
- - ~>
|
@@ -65,10 +85,15 @@ dependencies:
|
|
65
85
|
version: 1.0.0
|
66
86
|
type: :development
|
67
87
|
prerelease: false
|
68
|
-
version_requirements:
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.0.0
|
69
94
|
- !ruby/object:Gem::Dependency
|
70
95
|
name: em-synchrony
|
71
|
-
requirement:
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
72
97
|
none: false
|
73
98
|
requirements:
|
74
99
|
- - ~>
|
@@ -76,17 +101,22 @@ dependencies:
|
|
76
101
|
version: 1.0.0
|
77
102
|
type: :development
|
78
103
|
prerelease: false
|
79
|
-
version_requirements:
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 1.0.0
|
80
110
|
description: Cross server-process-fiber EventMachine + Redis based semaphore with
|
81
111
|
many features
|
82
112
|
email: rafal@yeondir.com
|
83
113
|
executables: []
|
84
114
|
extensions: []
|
85
115
|
extra_rdoc_files:
|
86
|
-
- README.
|
116
|
+
- README.md
|
87
117
|
files:
|
88
118
|
- HISTORY.rdoc
|
89
|
-
- README.
|
119
|
+
- README.md
|
90
120
|
- Rakefile
|
91
121
|
- lib/redis-em-mutex.rb
|
92
122
|
- lib/redis/em-mutex.rb
|
@@ -107,7 +137,7 @@ rdoc_options:
|
|
107
137
|
- --title
|
108
138
|
- redis-em-mutex
|
109
139
|
- --main
|
110
|
-
- README.
|
140
|
+
- README.md
|
111
141
|
require_paths:
|
112
142
|
- lib
|
113
143
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -125,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
155
|
requirements:
|
126
156
|
- Redis server 2.4+
|
127
157
|
rubyforge_project:
|
128
|
-
rubygems_version: 1.8.
|
158
|
+
rubygems_version: 1.8.25
|
129
159
|
signing_key:
|
130
160
|
specification_version: 3
|
131
161
|
summary: Cross server-process-fiber EventMachine + Redis based semaphore
|