redis-em-mutex 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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}"