redis-em-mutex 0.1.2 → 0.2.0

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.
@@ -22,6 +22,7 @@ class Redis
22
22
  #
23
23
  class Mutex
24
24
 
25
+ autoload :NS, 'redis/em-mutex/ns'
25
26
  autoload :Macro, 'redis/em-mutex/macro'
26
27
 
27
28
  module Errors
@@ -46,62 +47,34 @@ class Redis
46
47
  @@ns = nil
47
48
  @@uuid = nil
48
49
 
49
- attr_accessor :expire_timeout, :block_timeout
50
- attr_reader :names, :ns
50
+ attr_reader :names, :ns, :block_timeout
51
51
  alias_method :namespace, :ns
52
52
 
53
- class NS
54
- attr_reader :ns
55
- alias_method :namespace, :ns
56
- # Creates a new namespace (Mutex factory).
57
- #
58
- # - ns = namespace
59
- # - opts = options hash:
60
- # - :block - default block timeout
61
- # - :expire - default expire timeout
62
- def initialize(ns, opts = {})
63
- @ns = ns
64
- @opts = (opts || {}).merge(:ns => ns)
65
- end
66
-
67
- # Creates a namespaced cross machine/process/fiber semaphore.
68
- #
69
- # for arguments see: Redis::EM::Mutex.new
70
- def new(*args)
71
- if args.last.kind_of?(Hash)
72
- args[-1] = @opts.merge(args.last)
73
- else
74
- args.push @opts
75
- end
76
- Redis::EM::Mutex.new(*args)
77
- end
53
+ def expire_timeout; @expire_timeout || @@default_expire; end
78
54
 
79
- # Attempts to grab the lock and waits if it isn’t available.
80
- #
81
- # See: Redis::EM::Mutex.lock
82
- def lock(*args)
83
- mutex = new(*args)
84
- mutex if mutex.lock
85
- end
55
+ def expire_timeout=(value)
56
+ raise ArgumentError, "#{self.class.name}\#expire_timeout value must be greater than 0" unless (value = value.to_f) > 0
57
+ @expire_timeout = value
58
+ end
86
59
 
87
- # Executes block of code protected with namespaced semaphore.
88
- #
89
- # See: Redis::EM::Mutex.synchronize
90
- def synchronize(*args, &block)
91
- new(*args).synchronize(&block)
92
- end
60
+ def block_timeout=(value)
61
+ @block_timeout = value.nil? ? nil : value.to_f
93
62
  end
94
63
 
95
64
  # Creates a new cross machine/process/fiber semaphore
96
65
  #
97
- # Redis::EM::Mutex.new(*names, opts = {})
66
+ # Redis::EM::Mutex.new(*names, options = {})
98
67
  #
99
68
  # - *names = lock identifiers - if none they are auto generated
100
- # - opts = options hash:
69
+ # - options = hash:
101
70
  # - :name - same as *names (in case *names arguments were omitted)
102
71
  # - :block - default block timeout
103
72
  # - :expire - default expire timeout (see: Mutex#lock and Mutex#try_lock)
104
73
  # - :ns - local namespace (otherwise global namespace is used)
74
+ # - :owner - owner definition instead of Fiber#__id__
75
+ #
76
+ # Raises MutexError if used before Mutex.setup.
77
+ # Raises ArgumentError on invalid options.
105
78
  def initialize(*args)
106
79
  raise MutexError, "call #{self.class}::setup first" unless @@redis_pool
107
80
 
@@ -109,13 +82,23 @@ class Redis
109
82
 
110
83
  @names = args
111
84
  @names = Array(opts[:name] || "#{@@name_index.succ!}.lock") if @names.empty?
112
- raise MutexError, "semaphore names must not be empty" if @names.empty?
85
+ @slept = {}
86
+ raise ArgumentError, "semaphore names must not be empty" if @names.empty?
113
87
  @multi = !@names.one?
114
88
  @ns = opts[:ns] || @@ns
115
89
  @ns_names = @ns ? @names.map {|n| "#@ns:#{n}".freeze }.freeze : @names.map {|n| n.to_s.dup.freeze }.freeze
116
- @expire_timeout = opts[:expire]
117
- @block_timeout = opts[:block]
118
- @locked_id = nil
90
+ self.expire_timeout = opts[:expire] if opts.key?(:expire)
91
+ self.block_timeout = opts[:block] if opts.key?(:block)
92
+ @locked_owner_id = @locked_id = nil
93
+ if (owner = opts[:owner])
94
+ self.define_singleton_method(:owner_ident) do |lock_id = nil|
95
+ if lock_id
96
+ "#@@uuid$#$$@#{owner} #{lock_id}"
97
+ else
98
+ "#@@uuid$#$$@#{owner}"
99
+ end
100
+ end
101
+ end
119
102
  end
120
103
 
121
104
  # Returns `true` if this semaphore (at least one of locked `names`) is currently being held by some owner.
@@ -129,49 +112,92 @@ class Redis
129
112
  end
130
113
  end
131
114
 
132
- # Returns `true` if this semaphore (all the locked `names`) is currently being held by calling fiber
133
- # (if executing fiber is the owner).
115
+ # Returns `true` if this semaphore (all the locked `names`) is currently being held by calling owner.
116
+ # This is the method you should use to check if lock is still held and valid.
134
117
  def owned?
135
- !!if @locked_id
136
- lock_full_ident = owner_ident(@locked_id)
118
+ !!if @locked_id && owner_ident(@locked_id) == (lock_full_ident = @locked_owner_id)
137
119
  @@redis_pool.mget(*@ns_names).all? {|v| v == lock_full_ident}
138
120
  end
139
121
  end
140
122
 
123
+ # Returns `true` when the semaphore is being held and have already expired.
124
+ # Returns `false` when the semaphore is still locked and valid
125
+ # or `nil` if the semaphore wasn't locked by current owner.
126
+ #
127
+ # The check is performed only on the Mutex object instance and should only be used as a hint.
128
+ # For reliable lock status information use #refresh or #owned? instead.
129
+ def expired?
130
+ Time.now.to_f > @locked_id.to_f if @locked_id && owner_ident(@locked_id) == @locked_owner_id
131
+ end
132
+
133
+ # Returns the number of seconds left until the semaphore expires.
134
+ # The number of seconds less than 0 means that the semaphore expired and could be grabbed
135
+ # by some other owner.
136
+ # Returns `nil` if the semaphore wasn't locked by current owner.
137
+ #
138
+ # The check is performed only on the Mutex object instance and should only be used as a hint.
139
+ # For reliable lock status information use #refresh or #owned? instead.
140
+ def expires_in
141
+ @locked_id.to_f - Time.now.to_f if @locked_id && owner_ident(@locked_id) == @locked_owner_id
142
+ end
143
+
144
+ # Returns local time at which the semaphore will expire or have expired.
145
+ # Returns `nil` if the semaphore wasn't locked by current owner.
146
+ #
147
+ # The check is performed only on the Mutex object instance and should only be used as a hint.
148
+ # For reliable lock status information use #refresh or #owned? instead.
149
+ def expires_at
150
+ Time.at(@locked_id.to_f) if @locked_id && owner_ident(@locked_id) == @locked_owner_id
151
+ end
152
+
153
+ # Returns timestamp at which the semaphore will expire or have expired.
154
+ # Returns `nil` if the semaphore wasn't locked by current owner.
155
+ #
156
+ # The check is performed only on the Mutex object instance and should only be used as a hint.
157
+ # For reliable lock status information use #refresh or #owned? instead.
158
+ def expiration_timestamp
159
+ @locked_id.to_f if @locked_id && owner_ident(@locked_id) == @locked_owner_id
160
+ end
161
+
141
162
  # Attempts to obtain the lock and returns immediately.
142
163
  # Returns `true` if the lock was granted.
143
- # Use Mutex#expire_timeout= to set custom lock expiration time in secods.
164
+ # Use Mutex#expire_timeout= to set lock expiration time in secods.
144
165
  # Otherwise global Mutex.default_expire is used.
145
166
  #
146
- # This method does not lock expired semaphores.
167
+ # This method doesn't capture expired semaphores
168
+ # and therefore it should not be used under normal circumstances.
147
169
  # Use Mutex#lock with block_timeout = 0 to obtain expired lock without blocking.
148
170
  def try_lock
149
- lock_id = (Time.now + (@expire_timeout.to_f.nonzero? || @@default_expire)).to_f.to_s
171
+ lock_id = (Time.now + expire_timeout).to_f.to_s
172
+ lock_full_ident = owner_ident(lock_id)
150
173
  !!if @multi
151
- lock_full_ident = owner_ident(lock_id)
152
174
  if @@redis_pool.msetnx(*@ns_names.map {|k| [k, lock_full_ident]}.flatten)
153
175
  @locked_id = lock_id
176
+ @locked_owner_id = lock_full_ident
154
177
  end
155
- elsif @@redis_pool.setnx(@ns_names.first, owner_ident(lock_id))
178
+ elsif @@redis_pool.setnx(@ns_names.first, lock_full_ident)
156
179
  @locked_id = lock_id
180
+ @locked_owner_id = lock_full_ident
157
181
  end
158
182
  end
159
183
 
160
184
  # Refreshes lock expiration timeout.
161
- # Returns true if refresh was successfull or false if mutex was not locked or has already expired.
185
+ # Returns `true` if refresh was successfull.
186
+ # Returns `false` if the semaphore wasn't locked or when it was locked but it has expired
187
+ # and now it's got a new owner.
162
188
  def refresh(expire_timeout=nil)
163
189
  ret = false
164
- if @locked_id
165
- new_lock_id = (Time.now + (expire_timeout.to_f.nonzero? || @expire_timeout.to_f.nonzero? || @@default_expire)).to_f.to_s
190
+ if @locked_id && owner_ident(@locked_id) == (lock_full_ident = @locked_owner_id)
191
+ new_lock_id = (Time.now + (expire_timeout.to_f.nonzero? || self.expire_timeout)).to_f.to_s
166
192
  new_lock_full_ident = owner_ident(new_lock_id)
167
- lock_full_ident = owner_ident(@locked_id)
168
193
  @@redis_pool.execute(false) do |r|
169
194
  r.watch(*@ns_names) do
170
195
  if r.mget(*@ns_names).all? {|v| v == lock_full_ident}
171
- ret = !!r.multi do |multi|
172
- multi.mset(*@ns_names.map {|k| [k, new_lock_full_ident]}.flatten)
196
+ if r.multi {|m| m.mset(*@ns_names.map {|k| [k, new_lock_full_ident]}.flatten)}
197
+ @locked_id = new_lock_id
198
+ @locked_owner_id = new_lock_full_ident
199
+ ret = true
173
200
  end
174
- @locked_id = new_lock_id if ret
175
201
  else
176
202
  r.unwatch
177
203
  end
@@ -181,16 +207,16 @@ class Redis
181
207
  ret
182
208
  end
183
209
 
184
- # Releases the lock unconditionally.
185
- # If semaphore wasnt locked by the current owner it is silently ignored.
186
- # Returns self.
187
- def unlock
188
- if @locked_id
189
- lock_full_ident = owner_ident(@locked_id)
210
+ # Releases the lock. Returns self on success.
211
+ # Returns `false` if the semaphore wasn't locked or when it was locked but it has expired
212
+ # and now it's got a new owner.
213
+ def unlock!
214
+ ret = false
215
+ if @locked_id && owner_ident(@locked_id) == (lock_full_ident = @locked_owner_id)
190
216
  @@redis_pool.execute(false) do |r|
191
217
  r.watch(*@ns_names) do
192
218
  if r.mget(*@ns_names).all? {|v| v == lock_full_ident}
193
- r.multi do |multi|
219
+ ret = !!r.multi do |multi|
194
220
  multi.del(*@ns_names)
195
221
  multi.publish SIGNAL_QUEUE_CHANNEL, Marshal.dump(@ns_names)
196
222
  end
@@ -199,7 +225,16 @@ class Redis
199
225
  end
200
226
  end
201
227
  end
228
+ @locked_owner_id = @locked_id = nil
202
229
  end
230
+ ret && self
231
+ end
232
+
233
+ # Releases the lock unconditionally.
234
+ # If the semaphore wasn’t locked by the current owner it is silently ignored.
235
+ # Returns self.
236
+ def unlock
237
+ unlock!
203
238
  self
204
239
  end
205
240
 
@@ -215,7 +250,7 @@ class Redis
215
250
  # Use Mutex#expire_timeout= to set lock expiration timeout.
216
251
  # Otherwise global Mutex.default_expire is used.
217
252
  def lock(block_timeout = nil)
218
- block_timeout||= @block_timeout
253
+ block_timeout||= self.block_timeout
219
254
  names = @ns_names
220
255
  timer = fiber = nil
221
256
  try_again = false
@@ -223,62 +258,97 @@ class Redis
223
258
  try_again = true
224
259
  ::EM.next_tick { fiber.resume if fiber } if fiber
225
260
  end
226
- queues = names.map {|n| @@signal_queue[n] << handler }
227
- ident_match = owner_ident
228
- until try_lock
229
- Mutex.start_watcher unless @@watching == $$
230
- start_time = Time.now.to_f
231
- expire_time = nil
232
- @@redis_pool.execute(false) do |r|
233
- r.watch(*names) do
234
- expired_names = names.zip(r.mget(*names)).map do |name, lock_value|
235
- if lock_value
236
- owner, exp_id = lock_value.split ' '
237
- exp_time = exp_id.to_f
238
- expire_time = exp_time if expire_time.nil? || exp_time < expire_time
239
- raise MutexError, "deadlock; recursive locking #{owner}" if owner == ident_match
240
- if exp_time < start_time
241
- name
261
+ begin
262
+ queues = names.map {|n| @@signal_queue[n] << handler }
263
+ ident_match = owner_ident
264
+ until try_lock
265
+ Mutex.start_watcher unless watching?
266
+ start_time = Time.now.to_f
267
+ expire_time = nil
268
+ @@redis_pool.execute(false) do |r|
269
+ r.watch(*names) do
270
+ expired_names = names.zip(r.mget(*names)).map do |name, lock_value|
271
+ if lock_value
272
+ owner, exp_id = lock_value.split ' '
273
+ exp_time = exp_id.to_f
274
+ expire_time = exp_time if expire_time.nil? || exp_time < expire_time
275
+ raise MutexError, "deadlock; recursive locking #{owner}" if owner == ident_match
276
+ if exp_time < start_time
277
+ name
278
+ end
242
279
  end
243
280
  end
244
- end
245
- if expire_time && expire_time < start_time
246
- r.multi do |multi|
247
- expired_names = expired_names.compact
248
- multi.del(*expired_names)
249
- multi.publish SIGNAL_QUEUE_CHANNEL, Marshal.dump(expired_names)
281
+ if expire_time && expire_time < start_time
282
+ r.multi do |multi|
283
+ expired_names = expired_names.compact
284
+ multi.del(*expired_names)
285
+ multi.publish SIGNAL_QUEUE_CHANNEL, Marshal.dump(expired_names)
286
+ end
287
+ else
288
+ r.unwatch
250
289
  end
251
- else
252
- r.unwatch
253
290
  end
254
291
  end
255
- end
256
- timeout = (expire_time = expire_time.to_f) - start_time
257
- timeout = block_timeout if block_timeout && block_timeout < timeout
292
+ timeout = (expire_time = expire_time.to_f) - start_time
293
+ timeout = block_timeout if block_timeout && block_timeout < timeout
258
294
 
259
- if !try_again && timeout > 0
260
- timer = ::EM::Timer.new(timeout) do
261
- timer = nil
262
- ::EM.next_tick { fiber.resume if fiber } if fiber
295
+ if !try_again && timeout > 0
296
+ timer = ::EM::Timer.new(timeout) do
297
+ timer = nil
298
+ ::EM.next_tick { fiber.resume if fiber } if fiber
299
+ end
300
+ fiber = Fiber.current
301
+ Fiber.yield
302
+ fiber = nil
303
+ end
304
+ finish_time = Time.now.to_f
305
+ if try_again || finish_time > expire_time
306
+ block_timeout-= finish_time - start_time if block_timeout
307
+ try_again = false
308
+ else
309
+ return false
263
310
  end
264
- fiber = Fiber.current
265
- Fiber.yield
266
- fiber = nil
267
311
  end
268
- finish_time = Time.now.to_f
269
- if try_again || finish_time > expire_time
270
- block_timeout-= finish_time - start_time if block_timeout
271
- try_again = false
272
- else
273
- return false
312
+ true
313
+ ensure
314
+ timer.cancel if timer
315
+ timer = nil
316
+ queues.each {|q| q.delete handler }
317
+ names.each {|n| @@signal_queue.delete(n) if @@signal_queue[n].empty? }
318
+ end
319
+ end
320
+
321
+ # Wakes up currently sleeping fiber on a mutex.
322
+ def wakeup(fiber)
323
+ fiber.resume if @slept.delete(fiber)
324
+ end
325
+
326
+ # for compatibility with EventMachine::Synchrony::Thread::ConditionVariable
327
+ alias_method :_wakeup, :wakeup
328
+
329
+ # Releases the lock and sleeps `timeout` seconds if it is given and non-nil or forever.
330
+ # Raises MutexError if mutex wasn’t locked by the current owner.
331
+ # Raises MutexTimeout if #block_timeout= was set and timeout
332
+ # occured while locking after sleep.
333
+ # If code block is provided it is executed after waking up, just before grabbing a lock.
334
+ def sleep(timeout = nil)
335
+ raise MutexError, "can't sleep #{self.class} wasn't locked" unless unlock!
336
+ start = Time.now
337
+ current = Fiber.current
338
+ @slept[current] = true
339
+ if timeout
340
+ timer = ::EM.add_timer(timeout) do
341
+ wakeup(current)
274
342
  end
343
+ Fiber.yield
344
+ ::EM.cancel_timer timer
345
+ else
346
+ Fiber.yield
275
347
  end
276
- true
277
- ensure
278
- timer.cancel if timer
279
- timer = nil
280
- queues.each {|q| q.delete handler }
281
- names.each {|n| @@signal_queue.delete(n) if @@signal_queue[n].empty? }
348
+ @slept.delete current
349
+ yield if block_given?
350
+ raise MutexTimeout unless lock
351
+ Time.now - start
282
352
  end
283
353
 
284
354
  # Execute block of code protected with semaphore.
@@ -288,7 +358,7 @@ class Redis
288
358
  # If `block_timeout` or Mutex#block_timeout is set and
289
359
  # lock isn't obtained within `block_timeout` seconds this method raises
290
360
  # MutexTimeout.
291
- def synchronize(block_timeout = nil)
361
+ def synchronize(block_timeout=nil)
292
362
  if lock(block_timeout)
293
363
  begin
294
364
  yield self
@@ -300,18 +370,27 @@ class Redis
300
370
  end
301
371
  end
302
372
 
373
+ # Returns true if watcher is connected
374
+ def watching?; @@watching == $$; end
375
+
376
+ # Returns true if watcher is connected
377
+ def self.watching?; @@watching == $$; end
378
+
303
379
  class << self
304
380
  def ns; @@ns; end
305
381
  def ns=(namespace); @@ns = namespace; end
306
382
  alias_method :namespace, :ns
307
383
  alias_method :'namespace=', :'ns='
308
-
384
+
309
385
  # Default value of expiration timeout in seconds.
310
386
  def default_expire; @@default_expire; end
311
-
387
+
312
388
  # Assigns default value of expiration timeout in seconds.
313
389
  # Must be > 0.
314
- def default_expire=(value); @@default_expire=value.to_f.abs; end
390
+ def default_expire=(value)
391
+ raise ArgumentError, "#{name}.default_expire value must be greater than 0" unless (value = value.to_f) > 0
392
+ @@default_expire = value
393
+ end
315
394
 
316
395
  # Setup redis database and other defaults
317
396
  # MUST BE called once before any semaphore is created.
@@ -414,8 +493,8 @@ class Redis
414
493
  # Mutex.setup for "lightweight" startup procedure.
415
494
  def start_watcher
416
495
  raise MutexError, "call #{self.class}::setup first" unless @@redis_watcher
417
- return if @@watching == $$
418
- if @@watching
496
+ return if watching?
497
+ if @@watching # Process id changed, we've been forked alive!
419
498
  @@redis_watcher = Redis.new @redis_options
420
499
  @@signal_queue.clear
421
500
  end
@@ -456,16 +535,17 @@ class Redis
456
535
  else
457
536
  sleep retries > 1 ? 1 : 0.1
458
537
  end
459
- end while @@watching == $$
538
+ end while watching?
460
539
  end.resume
461
540
  until @@watcher_subscribed
462
- raise MutexError, "Can not establish watcher channel connection!" unless @@watching == $$
541
+ raise MutexError, "Can not establish watcher channel connection!" unless watching?
463
542
  fiber = Fiber.current
464
543
  ::EM.next_tick { fiber.resume }
465
544
  Fiber.yield
466
545
  end
467
546
  end
468
547
 
548
+ # EM sleep helper
469
549
  def sleep(seconds)
470
550
  fiber = Fiber.current
471
551
  ::EM::Timer.new(secs) { fiber.resume }
@@ -480,7 +560,7 @@ class Redis
480
560
  # Pass `true` to forcefully stop it. This might instead cause
481
561
  # MutexError to be raised in waiting fibers.
482
562
  def stop_watcher(force = false)
483
- return unless @@watching == $$
563
+ return unless watching?
484
564
  raise MutexError, "call #{self.class}::setup first" unless @@redis_watcher
485
565
  unless @@signal_queue.empty? || force
486
566
  raise MutexError, "can't stop: active signal queue handlers"
@@ -504,14 +584,16 @@ class Redis
504
584
  end
505
585
 
506
586
  # Attempts to grab the lock and waits if it isn’t available.
507
- # Raises MutexError if mutex was locked by the current owner.
587
+ # Raises MutexError if mutex was locked by the current owner
588
+ # or if used before Mutex.setup.
589
+ # Raises ArgumentError on invalid options.
508
590
  # Returns instance of Redis::EM::Mutex if lock was successfully obtained.
509
591
  # Returns `nil` if lock wasn't available within `:block` seconds.
510
592
  #
511
- # Redis::EM::Mutex.lock(*names, opts = {})
593
+ # Redis::EM::Mutex.lock(*names, options = {})
512
594
  #
513
595
  # - *names = lock identifiers - if none they are auto generated
514
- # - opts = options hash:
596
+ # - options = hash:
515
597
  # - :name - same as name (in case *names arguments were omitted)
516
598
  # - :block - block timeout
517
599
  # - :expire - expire timeout (see: Mutex#lock and Mutex#try_lock)
@@ -520,13 +602,14 @@ class Redis
520
602
  mutex = new(*args)
521
603
  mutex if mutex.lock
522
604
  end
605
+
523
606
  # Execute block of code protected with named semaphore.
524
607
  # Returns result of code block.
525
608
  #
526
- # Redis::EM::Mutex.synchronize(*names, opts = {}, &block)
609
+ # Redis::EM::Mutex.synchronize(*names, options = {}, &block)
527
610
  #
528
611
  # - *names = lock identifiers - if none they are auto generated
529
- # - opts = options hash:
612
+ # - options = hash:
530
613
  # - :name - same as name (in case *names arguments were omitted)
531
614
  # - :block - block timeout
532
615
  # - :expire - expire timeout (see: Mutex#lock and Mutex#try_lock)
@@ -534,13 +617,13 @@ class Redis
534
617
  #
535
618
  # If `:block` is set and lock isn't obtained within `:block` seconds this method raises
536
619
  # MutexTimeout.
620
+ # Raises MutexError if used before Mutex.setup.
621
+ # Raises ArgumentError on invalid options.
537
622
  def synchronize(*args, &block)
538
623
  new(*args).synchronize(&block)
539
624
  end
540
625
  end
541
626
 
542
- private
543
-
544
627
  def owner_ident(lock_id = nil)
545
628
  if lock_id
546
629
  "#@@uuid$#$$@#{Fiber.current.__id__} #{lock_id}"