redis-em-mutex 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|