redis-em-mutex 0.1.0 → 0.1.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.
- data/HISTORY.rdoc +10 -0
- data/README.rdoc +2 -2
- data/lib/redis/em-mutex.rb +549 -555
- data/lib/redis-em-mutex.rb +5 -5
- data/spec/redis-em-mutex-namespaces.rb +113 -0
- data/spec/redis-em-mutex-semaphores.rb +184 -173
- metadata +16 -13
data/lib/redis/em-mutex.rb
CHANGED
@@ -1,555 +1,549 @@
|
|
1
|
-
# -*- coding: UTF-8 -*-
|
2
|
-
|
3
|
-
|
4
|
-
require '
|
5
|
-
require '
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
class
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
@@
|
33
|
-
@@
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
@@
|
38
|
-
@@
|
39
|
-
@@
|
40
|
-
@@
|
41
|
-
@@
|
42
|
-
@@
|
43
|
-
@@
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
#
|
54
|
-
#
|
55
|
-
# -
|
56
|
-
# -
|
57
|
-
# - :
|
58
|
-
|
59
|
-
|
60
|
-
@
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
#
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
mutex
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
# -
|
97
|
-
# -
|
98
|
-
# - :
|
99
|
-
# - :
|
100
|
-
# - :
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
@names =
|
108
|
-
|
109
|
-
|
110
|
-
@
|
111
|
-
@
|
112
|
-
@
|
113
|
-
@
|
114
|
-
@
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
#
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
#
|
181
|
-
#
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
multi.
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
#
|
203
|
-
#
|
204
|
-
# Returns `
|
205
|
-
#
|
206
|
-
#
|
207
|
-
# If
|
208
|
-
#
|
209
|
-
#
|
210
|
-
#
|
211
|
-
#
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
expired_names
|
244
|
-
multi.
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
timeout =
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
finish_time
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
timer
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
#
|
282
|
-
#
|
283
|
-
#
|
284
|
-
#
|
285
|
-
#
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
def ns; @@ns; end
|
301
|
-
|
302
|
-
alias_method :namespace, :ns
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
#
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
#
|
313
|
-
#
|
314
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
#
|
318
|
-
#
|
319
|
-
# - :
|
320
|
-
# - :
|
321
|
-
# - :
|
322
|
-
#
|
323
|
-
#
|
324
|
-
#
|
325
|
-
#
|
326
|
-
#
|
327
|
-
#
|
328
|
-
#
|
329
|
-
#
|
330
|
-
#
|
331
|
-
#
|
332
|
-
#
|
333
|
-
#
|
334
|
-
#
|
335
|
-
#
|
336
|
-
#
|
337
|
-
# - :
|
338
|
-
# - :
|
339
|
-
# - :
|
340
|
-
# - :
|
341
|
-
# - :
|
342
|
-
#
|
343
|
-
#
|
344
|
-
#
|
345
|
-
#
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
opts
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
:
|
356
|
-
:
|
357
|
-
:
|
358
|
-
:
|
359
|
-
:
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
@@
|
373
|
-
@@
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
@@
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
end
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
end
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
end
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
#
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
#
|
502
|
-
#
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
#
|
508
|
-
#
|
509
|
-
#
|
510
|
-
#
|
511
|
-
#
|
512
|
-
#
|
513
|
-
#
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
#
|
519
|
-
#
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
#
|
525
|
-
#
|
526
|
-
#
|
527
|
-
#
|
528
|
-
#
|
529
|
-
#
|
530
|
-
#
|
531
|
-
#
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
end
|
551
|
-
end
|
552
|
-
|
553
|
-
end
|
554
|
-
end
|
555
|
-
end
|
1
|
+
# -*- coding: UTF-8 -*-
|
2
|
+
require 'ostruct'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'redis/connection/synchrony' unless defined? Redis::Connection::Synchrony
|
5
|
+
require 'redis'
|
6
|
+
|
7
|
+
class Redis
|
8
|
+
module EM
|
9
|
+
# Cross machine-process-fiber EventMachine + Redis based semaphore.
|
10
|
+
#
|
11
|
+
# WARNING:
|
12
|
+
#
|
13
|
+
# Methods of this class are NOT thread-safe.
|
14
|
+
# They are machine/process/fiber-safe.
|
15
|
+
# All method calls must be invoked only from EventMachine's reactor thread.
|
16
|
+
#
|
17
|
+
# - The terms "lock" and "semaphore" used in documentation are synonims.
|
18
|
+
# - The term "owner" denotes a Ruby Fiber in some Process on some Machine.
|
19
|
+
#
|
20
|
+
class Mutex
|
21
|
+
VERSION = '0.1.1'
|
22
|
+
|
23
|
+
module Errors
|
24
|
+
class MutexError < RuntimeError; end
|
25
|
+
class MutexTimeout < MutexError; end
|
26
|
+
end
|
27
|
+
|
28
|
+
include Errors
|
29
|
+
extend Errors
|
30
|
+
|
31
|
+
@@connection_pool_class = nil
|
32
|
+
@@connection_retry_max = 10
|
33
|
+
@@default_expire = 3600*24
|
34
|
+
AUTO_NAME_SEED = '__@'
|
35
|
+
SIGNAL_QUEUE_CHANNEL = "::#{self.name}::"
|
36
|
+
@@name_index = AUTO_NAME_SEED
|
37
|
+
@@redis_pool = nil
|
38
|
+
@@redis_watcher = nil
|
39
|
+
@@watching = false
|
40
|
+
@@watcher_subscribed = false
|
41
|
+
@@signal_queue = Hash.new {|h,k| h[k] = []}
|
42
|
+
@@ns = nil
|
43
|
+
@@uuid = nil
|
44
|
+
|
45
|
+
attr_accessor :expire_timeout, :block_timeout
|
46
|
+
attr_reader :names, :ns
|
47
|
+
alias_method :namespace, :ns
|
48
|
+
|
49
|
+
class NS
|
50
|
+
attr_reader :ns
|
51
|
+
alias_method :namespace, :ns
|
52
|
+
# Creates a new namespace (Mutex factory).
|
53
|
+
#
|
54
|
+
# - ns = namespace
|
55
|
+
# - opts = options hash:
|
56
|
+
# - :block - default block timeout
|
57
|
+
# - :expire - default expire timeout
|
58
|
+
def initialize(ns, opts = {})
|
59
|
+
@ns = ns
|
60
|
+
@opts = (opts || {}).merge(:ns => ns)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Creates a namespaced cross machine/process/fiber semaphore.
|
64
|
+
#
|
65
|
+
# for arguments see: Redis::EM::Mutex.new
|
66
|
+
def new(*args)
|
67
|
+
if args.last.kind_of?(Hash)
|
68
|
+
args[-1] = @opts.merge(args.last)
|
69
|
+
else
|
70
|
+
args.push @opts
|
71
|
+
end
|
72
|
+
Redis::EM::Mutex.new(*args)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Attempts to grab the lock and waits if it isn’t available.
|
76
|
+
#
|
77
|
+
# See: Redis::EM::Mutex.lock
|
78
|
+
def lock(*args)
|
79
|
+
mutex = new(*args)
|
80
|
+
mutex if mutex.lock
|
81
|
+
end
|
82
|
+
|
83
|
+
# Executes block of code protected with namespaced semaphore.
|
84
|
+
#
|
85
|
+
# See: Redis::EM::Mutex.synchronize
|
86
|
+
def synchronize(*args, &block)
|
87
|
+
new(*args).synchronize(&block)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Creates a new cross machine/process/fiber semaphore
|
92
|
+
#
|
93
|
+
# Redis::EM::Mutex.new(*names, opts = {})
|
94
|
+
#
|
95
|
+
# - *names = lock identifiers - if none they are auto generated
|
96
|
+
# - opts = options hash:
|
97
|
+
# - :name - same as *names (in case *names arguments were omitted)
|
98
|
+
# - :block - default block timeout
|
99
|
+
# - :expire - default expire timeout (see: Mutex#lock and Mutex#try_lock)
|
100
|
+
# - :ns - local namespace (otherwise global namespace is used)
|
101
|
+
def initialize(*args)
|
102
|
+
raise MutexError, "call #{self.class}::setup first" unless @@redis_pool
|
103
|
+
|
104
|
+
opts = args.last.kind_of?(Hash) ? args.pop : {}
|
105
|
+
|
106
|
+
@names = args
|
107
|
+
@names = Array(opts[:name] || "#{@@name_index.succ!}.lock") if @names.empty?
|
108
|
+
raise MutexError, "semaphore names must not be empty" if @names.empty?
|
109
|
+
@multi = !@names.one?
|
110
|
+
@ns = opts[:ns] || @@ns
|
111
|
+
@ns_names = @ns ? @names.map {|n| "#@ns:#{n}".freeze }.freeze : @names.map {|n| n.to_s.dup.freeze }.freeze
|
112
|
+
@expire_timeout = opts[:expire]
|
113
|
+
@block_timeout = opts[:block]
|
114
|
+
@locked_id = nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns `true` if this semaphore (at least one of locked `names`) is currently being held by some owner.
|
118
|
+
def locked?
|
119
|
+
if @multi
|
120
|
+
@@redis_pool.multi do |multi|
|
121
|
+
@ns_names.each {|n| multi.exists n}
|
122
|
+
end.any?
|
123
|
+
else
|
124
|
+
@@redis_pool.exists @ns_names.first
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns `true` if this semaphore (all the locked `names`) is currently being held by calling fiber.
|
129
|
+
def owned?
|
130
|
+
!!if @locked_id
|
131
|
+
lock_full_ident = owner_ident(@locked_id)
|
132
|
+
@@redis_pool.mget(*@ns_names).all? {|v| v == lock_full_ident}
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Attempts to obtain the lock and returns immediately.
|
137
|
+
# Returns `true` if the lock was granted.
|
138
|
+
# Use Mutex#expire_timeout= to set custom lock expiration time in secods.
|
139
|
+
# Otherwise global Mutex.default_expire is used.
|
140
|
+
#
|
141
|
+
# This method does not lock expired semaphores.
|
142
|
+
# Use Mutex#lock with block_timeout = 0 to obtain expired lock without blocking.
|
143
|
+
def try_lock
|
144
|
+
lock_id = (Time.now + (@expire_timeout.to_f.nonzero? || @@default_expire)).to_f.to_s
|
145
|
+
!!if @multi
|
146
|
+
lock_full_ident = owner_ident(lock_id)
|
147
|
+
if @@redis_pool.msetnx(*@ns_names.map {|k| [k, lock_full_ident]}.flatten)
|
148
|
+
@locked_id = lock_id
|
149
|
+
end
|
150
|
+
elsif @@redis_pool.setnx(@ns_names.first, owner_ident(lock_id))
|
151
|
+
@locked_id = lock_id
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Refreshes lock expiration timeout.
|
156
|
+
# Returns true if refresh was successfull or false if mutex was not locked or has already expired.
|
157
|
+
def refresh(expire_timeout=nil)
|
158
|
+
ret = false
|
159
|
+
if @locked_id
|
160
|
+
new_lock_id = (Time.now + (expire_timeout.to_f.nonzero? || @expire_timeout.to_f.nonzero? || @@default_expire)).to_f.to_s
|
161
|
+
new_lock_full_ident = owner_ident(new_lock_id)
|
162
|
+
lock_full_ident = owner_ident(@locked_id)
|
163
|
+
@@redis_pool.execute(false) do |r|
|
164
|
+
r.watch(*@ns_names) do
|
165
|
+
if r.mget(*@ns_names).all? {|v| v == lock_full_ident}
|
166
|
+
ret = !!r.multi do |multi|
|
167
|
+
multi.mset(*@ns_names.map {|k| [k, new_lock_full_ident]}.flatten)
|
168
|
+
end
|
169
|
+
@locked_id = new_lock_id if ret
|
170
|
+
else
|
171
|
+
r.unwatch
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
ret
|
177
|
+
end
|
178
|
+
|
179
|
+
# Releases the lock unconditionally.
|
180
|
+
# If semaphore wasn’t locked by the current owner it is silently ignored.
|
181
|
+
# Returns self.
|
182
|
+
def unlock
|
183
|
+
if @locked_id
|
184
|
+
lock_full_ident = owner_ident(@locked_id)
|
185
|
+
@@redis_pool.execute(false) do |r|
|
186
|
+
r.watch(*@ns_names) do
|
187
|
+
if r.mget(*@ns_names).all? {|v| v == lock_full_ident}
|
188
|
+
r.multi do |multi|
|
189
|
+
multi.del(*@ns_names)
|
190
|
+
multi.publish SIGNAL_QUEUE_CHANNEL, Marshal.dump(@ns_names)
|
191
|
+
end
|
192
|
+
else
|
193
|
+
r.unwatch
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
self
|
199
|
+
end
|
200
|
+
|
201
|
+
# Attempts to grab the lock and waits if it isn’t available.
|
202
|
+
# Raises MutexError if mutex was locked by the current owner.
|
203
|
+
# Returns `true` if lock was successfully obtained.
|
204
|
+
# Returns `false` if lock wasn't available within `block_timeout` seconds.
|
205
|
+
#
|
206
|
+
# If `block_timeout` is `nil` or omited this method uses Mutex#block_timeout.
|
207
|
+
# If also Mutex#block_timeout is nil this method returns only after lock
|
208
|
+
# has been granted.
|
209
|
+
#
|
210
|
+
# Use Mutex#expire_timeout= to set lock expiration timeout.
|
211
|
+
# Otherwise global Mutex.default_expire is used.
|
212
|
+
def lock(block_timeout = nil)
|
213
|
+
block_timeout||= @block_timeout
|
214
|
+
names = @ns_names
|
215
|
+
timer = fiber = nil
|
216
|
+
try_again = false
|
217
|
+
handler = proc do
|
218
|
+
try_again = true
|
219
|
+
::EM.next_tick { fiber.resume if fiber } if fiber
|
220
|
+
end
|
221
|
+
queues = names.map {|n| @@signal_queue[n] << handler }
|
222
|
+
ident_match = owner_ident
|
223
|
+
until try_lock
|
224
|
+
Mutex.start_watcher unless @@watching == $$
|
225
|
+
start_time = Time.now.to_f
|
226
|
+
expire_time = nil
|
227
|
+
@@redis_pool.execute(false) do |r|
|
228
|
+
r.watch(*names) do
|
229
|
+
expired_names = names.zip(r.mget(*names)).map do |name, lock_value|
|
230
|
+
if lock_value
|
231
|
+
owner, exp_id = lock_value.split ' '
|
232
|
+
exp_time = exp_id.to_f
|
233
|
+
expire_time = exp_time if expire_time.nil? || exp_time < expire_time
|
234
|
+
raise MutexError, "deadlock; recursive locking #{owner}" if owner == ident_match
|
235
|
+
if exp_time < start_time
|
236
|
+
name
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
if expire_time && expire_time < start_time
|
241
|
+
r.multi do |multi|
|
242
|
+
expired_names = expired_names.compact
|
243
|
+
multi.del(*expired_names)
|
244
|
+
multi.publish SIGNAL_QUEUE_CHANNEL, Marshal.dump(expired_names)
|
245
|
+
end
|
246
|
+
else
|
247
|
+
r.unwatch
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
timeout = (expire_time = expire_time.to_f) - start_time
|
252
|
+
timeout = block_timeout if block_timeout && block_timeout < timeout
|
253
|
+
|
254
|
+
if !try_again && timeout > 0
|
255
|
+
timer = ::EM::Timer.new(timeout) do
|
256
|
+
timer = nil
|
257
|
+
::EM.next_tick { fiber.resume if fiber } if fiber
|
258
|
+
end
|
259
|
+
fiber = Fiber.current
|
260
|
+
Fiber.yield
|
261
|
+
fiber = nil
|
262
|
+
end
|
263
|
+
finish_time = Time.now.to_f
|
264
|
+
if try_again || finish_time > expire_time
|
265
|
+
block_timeout-= finish_time - start_time if block_timeout
|
266
|
+
try_again = false
|
267
|
+
else
|
268
|
+
return false
|
269
|
+
end
|
270
|
+
end
|
271
|
+
true
|
272
|
+
ensure
|
273
|
+
timer.cancel if timer
|
274
|
+
timer = nil
|
275
|
+
queues.each {|q| q.delete handler }
|
276
|
+
names.each {|n| @@signal_queue.delete(n) if @@signal_queue[n].empty? }
|
277
|
+
end
|
278
|
+
|
279
|
+
# Execute block of code protected with semaphore.
|
280
|
+
# Code block receives mutex object.
|
281
|
+
# Returns result of code block.
|
282
|
+
#
|
283
|
+
# If `block_timeout` or Mutex#block_timeout is set and
|
284
|
+
# lock isn't obtained within `block_timeout` seconds this method raises
|
285
|
+
# MutexTimeout.
|
286
|
+
def synchronize(block_timeout = nil)
|
287
|
+
if lock(block_timeout)
|
288
|
+
begin
|
289
|
+
yield self
|
290
|
+
ensure
|
291
|
+
unlock
|
292
|
+
end
|
293
|
+
else
|
294
|
+
raise MutexTimeout
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
class << self
|
299
|
+
def ns; @@ns; end
|
300
|
+
def ns=(namespace); @@ns = namespace; end
|
301
|
+
alias_method :namespace, :ns
|
302
|
+
alias_method :'namespace=', :'ns='
|
303
|
+
|
304
|
+
# Default value of expiration timeout in seconds.
|
305
|
+
def default_expire; @@default_expire; end
|
306
|
+
|
307
|
+
# Assigns default value of expiration timeout in seconds.
|
308
|
+
# Must be > 0.
|
309
|
+
def default_expire=(value); @@default_expire=value.to_f.abs; end
|
310
|
+
|
311
|
+
# Setup redis database and other defaults
|
312
|
+
# MUST BE called once before any semaphore is created.
|
313
|
+
#
|
314
|
+
# opts = options Hash:
|
315
|
+
#
|
316
|
+
# global options:
|
317
|
+
#
|
318
|
+
# - :connection_pool_class - default is ::EM::Synchrony::ConnectionPool
|
319
|
+
# - :expire - sets global Mutex.default_expire
|
320
|
+
# - :ns - sets global Mutex.namespace
|
321
|
+
# - :reconnect_max - maximum num. of attempts to re-establish
|
322
|
+
# connection to redis server;
|
323
|
+
# default is 10; set to 0 to disable re-connecting;
|
324
|
+
# set to -1 to attempt forever
|
325
|
+
#
|
326
|
+
# redis connection options:
|
327
|
+
#
|
328
|
+
# - :size - redis connection pool size
|
329
|
+
#
|
330
|
+
# passed directly to Redis.new:
|
331
|
+
#
|
332
|
+
# - :url - redis server url
|
333
|
+
#
|
334
|
+
# or
|
335
|
+
#
|
336
|
+
# - :scheme - "redis" or "unix"
|
337
|
+
# - :host - redis host
|
338
|
+
# - :port - redis port
|
339
|
+
# - :password - redis password
|
340
|
+
# - :db - redis database number
|
341
|
+
# - :path - redis unix-socket path
|
342
|
+
#
|
343
|
+
# or
|
344
|
+
#
|
345
|
+
# - :redis - initialized ConnectionPool of Redis clients.
|
346
|
+
def setup(opts = {})
|
347
|
+
stop_watcher
|
348
|
+
opts = OpenStruct.new(opts)
|
349
|
+
yield opts if block_given?
|
350
|
+
@@connection_pool_class = opts.connection_pool_class if opts.connection_pool_class.kind_of?(Class)
|
351
|
+
@redis_options = redis_options = {:driver => :synchrony}
|
352
|
+
redis_updater = proc do |redis|
|
353
|
+
redis_options.update({
|
354
|
+
:scheme => redis.scheme,
|
355
|
+
:host => redis.host,
|
356
|
+
:port => redis.port,
|
357
|
+
:password => redis.password,
|
358
|
+
:db => redis.db,
|
359
|
+
:path => redis.path
|
360
|
+
}.reject {|_k, v| v.nil?})
|
361
|
+
end
|
362
|
+
if (redis = opts.redis) && !opts.url
|
363
|
+
redis_updater.call redis
|
364
|
+
elsif opts.url
|
365
|
+
redis_options[:url] = opts.url
|
366
|
+
end
|
367
|
+
redis_updater.call opts
|
368
|
+
namespace = opts.ns
|
369
|
+
pool_size = (opts.size.to_i.nonzero? || 1).abs
|
370
|
+
self.default_expire = opts.expire if opts.expire
|
371
|
+
@@connection_retry_max = opts.reconnect_max.to_i if opts.reconnect_max
|
372
|
+
@@ns = namespace if namespace
|
373
|
+
@@uuid = if SecureRandom.respond_to?(:uuid)
|
374
|
+
SecureRandom.uuid
|
375
|
+
else
|
376
|
+
SecureRandom.base64(24)
|
377
|
+
end
|
378
|
+
unless (@@redis_pool = redis)
|
379
|
+
unless @@connection_pool_class
|
380
|
+
begin
|
381
|
+
require 'em-synchrony/connection_pool' unless defined?(::EM::Synchrony::ConnectionPool)
|
382
|
+
rescue LoadError
|
383
|
+
raise ":connection_pool_class required; could not fall back to EM::Synchrony::ConnectionPool - gem install em-synchrony"
|
384
|
+
end
|
385
|
+
@@connection_pool_class = ::EM::Synchrony::ConnectionPool
|
386
|
+
end
|
387
|
+
@@redis_pool = @@connection_pool_class.new(size: pool_size) do
|
388
|
+
Redis.new redis_options
|
389
|
+
end
|
390
|
+
end
|
391
|
+
@@redis_watcher = Redis.new redis_options
|
392
|
+
start_watcher if ::EM.reactor_running?
|
393
|
+
end
|
394
|
+
|
395
|
+
# resets Mutex's automatic name generator
|
396
|
+
def reset_autoname
|
397
|
+
@@name_index = AUTO_NAME_SEED
|
398
|
+
end
|
399
|
+
|
400
|
+
def wakeup_queue_all
|
401
|
+
@@signal_queue.each_value do |queue|
|
402
|
+
queue.each {|h| h.call }
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# Initializes the "unlock" channel watcher. It's called by Mutex.setup
|
407
|
+
# internally. Should not be used under normal circumstances.
|
408
|
+
# If EventMachine is to be re-started (or after EM.fork_reactor) this method may be used instead of
|
409
|
+
# Mutex.setup for "lightweight" startup procedure.
|
410
|
+
def start_watcher
|
411
|
+
raise MutexError, "call #{self.class}::setup first" unless @@redis_watcher
|
412
|
+
return if @@watching == $$
|
413
|
+
if @@watching
|
414
|
+
@@redis_watcher = Redis.new @redis_options
|
415
|
+
@@signal_queue.clear
|
416
|
+
end
|
417
|
+
@@watching = $$
|
418
|
+
retries = 0
|
419
|
+
Fiber.new do
|
420
|
+
begin
|
421
|
+
@@redis_watcher.subscribe(SIGNAL_QUEUE_CHANNEL) do |on|
|
422
|
+
on.subscribe do |channel,|
|
423
|
+
if channel == SIGNAL_QUEUE_CHANNEL
|
424
|
+
@@watcher_subscribed = true
|
425
|
+
retries = 0
|
426
|
+
wakeup_queue_all
|
427
|
+
end
|
428
|
+
end
|
429
|
+
on.message do |channel, message|
|
430
|
+
if channel == SIGNAL_QUEUE_CHANNEL
|
431
|
+
handlers = {}
|
432
|
+
Marshal.load(message).each do |name|
|
433
|
+
handlers[@@signal_queue[name].first] = true if @@signal_queue.key?(name)
|
434
|
+
end
|
435
|
+
handlers.keys.each do |handler|
|
436
|
+
handler.call if handler
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
on.unsubscribe do |channel,|
|
441
|
+
@@watcher_subscribed = false if channel == SIGNAL_QUEUE_CHANNEL
|
442
|
+
end
|
443
|
+
end
|
444
|
+
break
|
445
|
+
rescue Redis::BaseConnectionError, EventMachine::ConnectionError => e
|
446
|
+
@@watcher_subscribed = false
|
447
|
+
warn e.message
|
448
|
+
retries+= 1
|
449
|
+
if retries > @@connection_retry_max && @@connection_retry_max >= 0
|
450
|
+
@@watching = false
|
451
|
+
else
|
452
|
+
sleep retries > 1 ? 1 : 0.1
|
453
|
+
end
|
454
|
+
end while @@watching == $$
|
455
|
+
end.resume
|
456
|
+
until @@watcher_subscribed
|
457
|
+
raise MutexError, "Can not establish watcher channel connection!" unless @@watching == $$
|
458
|
+
fiber = Fiber.current
|
459
|
+
::EM.next_tick { fiber.resume }
|
460
|
+
Fiber.yield
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def sleep(seconds)
|
465
|
+
fiber = Fiber.current
|
466
|
+
::EM::Timer.new(secs) { fiber.resume }
|
467
|
+
Fiber.yield
|
468
|
+
end
|
469
|
+
|
470
|
+
# Stops the watcher of the "unlock" channel.
|
471
|
+
# It should be called before stoping EvenMachine otherwise
|
472
|
+
# EM might wait forever for channel connection to be closed.
|
473
|
+
#
|
474
|
+
# Raises MutexError if there are still some fibers waiting for a lock.
|
475
|
+
# Pass `true` to forcefully stop it. This might instead cause
|
476
|
+
# MutexError to be raised in waiting fibers.
|
477
|
+
def stop_watcher(force = false)
|
478
|
+
return unless @@watching == $$
|
479
|
+
raise MutexError, "call #{self.class}::setup first" unless @@redis_watcher
|
480
|
+
unless @@signal_queue.empty? || force
|
481
|
+
raise MutexError, "can't stop: active signal queue handlers"
|
482
|
+
end
|
483
|
+
@@watching = false
|
484
|
+
if @@watcher_subscribed
|
485
|
+
@@redis_watcher.unsubscribe SIGNAL_QUEUE_CHANNEL
|
486
|
+
while @@watcher_subscribed
|
487
|
+
fiber = Fiber.current
|
488
|
+
::EM.next_tick { fiber.resume }
|
489
|
+
Fiber.yield
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
# Remove all current Machine/Process locks.
|
495
|
+
# Since there is no lock tracking mechanism, it might not be implemented easily.
|
496
|
+
# If the need arises then it probably should be implemented.
|
497
|
+
def sweep
|
498
|
+
raise NotImplementedError
|
499
|
+
end
|
500
|
+
|
501
|
+
# Attempts to grab the lock and waits if it isn’t available.
|
502
|
+
# Raises MutexError if mutex was locked by the current owner.
|
503
|
+
# Returns instance of Redis::EM::Mutex if lock was successfully obtained.
|
504
|
+
# Returns `nil` if lock wasn't available within `:block` seconds.
|
505
|
+
#
|
506
|
+
# Redis::EM::Mutex.lock(*names, opts = {})
|
507
|
+
#
|
508
|
+
# - *names = lock identifiers - if none they are auto generated
|
509
|
+
# - opts = options hash:
|
510
|
+
# - :name - same as name (in case *names arguments were omitted)
|
511
|
+
# - :block - block timeout
|
512
|
+
# - :expire - expire timeout (see: Mutex#lock and Mutex#try_lock)
|
513
|
+
# - :ns - namespace (otherwise global namespace is used)
|
514
|
+
def lock(*args)
|
515
|
+
mutex = new(*args)
|
516
|
+
mutex if mutex.lock
|
517
|
+
end
|
518
|
+
# Execute block of code protected with named semaphore.
|
519
|
+
# Returns result of code block.
|
520
|
+
#
|
521
|
+
# Redis::EM::Mutex.synchronize(*names, opts = {}, &block)
|
522
|
+
#
|
523
|
+
# - *names = lock identifiers - if none they are auto generated
|
524
|
+
# - opts = options hash:
|
525
|
+
# - :name - same as name (in case *names arguments were omitted)
|
526
|
+
# - :block - block timeout
|
527
|
+
# - :expire - expire timeout (see: Mutex#lock and Mutex#try_lock)
|
528
|
+
# - :ns - namespace (otherwise global namespace is used)
|
529
|
+
#
|
530
|
+
# If `:block` is set and lock isn't obtained within `:block` seconds this method raises
|
531
|
+
# MutexTimeout.
|
532
|
+
def synchronize(*args, &block)
|
533
|
+
new(*args).synchronize(&block)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
private
|
538
|
+
|
539
|
+
def owner_ident(lock_id = nil)
|
540
|
+
if lock_id
|
541
|
+
"#@@uuid$#$$@#{Fiber.current.__id__} #{lock_id}"
|
542
|
+
else
|
543
|
+
"#@@uuid$#$$@#{Fiber.current.__id__}"
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|