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