redis-em-mutex 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,179 @@
1
+ = redis-em-mutex
2
+
3
+ Author:: Rafał Michalski (mailto:rafal@yeondir.com)
4
+
5
+ * http://github.com/royaltm/redis-em-mutex
6
+
7
+ == DESCRIPTION
8
+
9
+ *redis-em-mutex* is the cross server-process-fiber EventMachine + Redis based semaphore.
10
+
11
+ == FEATURES
12
+
13
+ * only for EventMachine
14
+ * no CPU-intensive sleep/polling while waiting for lock to become available
15
+ * fibers waiting for the lock are signalled via Redis channel as soon as the lock
16
+ has been released (~< 1 ms)
17
+ * multi-locks (all-or-nothing) locking (to prevent possible deadlocks when
18
+ multiple semaphores are required to be locked at once)
19
+ * best served with EM-Synchrony (uses EM::Synchrony::ConnectionPool internally)
20
+ * fiber-safe
21
+ * deadlock detection (only trivial cases: locking twice the same resource from the same fiber)
22
+ * mandatory lock expiration (with refreshing)
23
+
24
+ == BUGS/LIMITATIONS
25
+
26
+ * only for EventMachine
27
+ * NOT thread-safe
28
+ * locking order between concurrent processes is undetermined (no FIFO)
29
+ * its not nifty, rather somewhat complicated
30
+
31
+ == REQUIREMENTS
32
+
33
+ * ruby >= 1.9 (tested: 1.9.3-p194, 1.9.2-p320, 1.9.1-p378)
34
+ * http://github.com/redis/redis-rb >= 3.0.1
35
+ * http://rubyeventmachine.com >= 0.12.10
36
+ * (optional) http://github.com/igrigorik/em-synchrony
37
+
38
+ == INSTALL
39
+
40
+ $ [sudo] gem install redis-em-mutex
41
+
42
+ ==== Gemfile
43
+
44
+ gem "redis-em-mutex", "~> 0.1.0"
45
+
46
+ ==== Github
47
+
48
+ git clone git://github.com/royaltm/redis-em-mutex.git
49
+
50
+ == USAGE
51
+
52
+ require 'em-synchrony'
53
+ require 'em-redis-mutex'
54
+
55
+ Redis::EM::Mutex.setup(size: 10, url: 'redis:///1', expire: 600)
56
+
57
+ # or
58
+
59
+ Redis::EM::Mutex.setup do |opts|
60
+ opts.size = 10
61
+ opts.url = 'redis:///1'
62
+ ...
63
+ end
64
+
65
+
66
+ EM.synchrony do
67
+ Redis::EM::Mutex.synchronize('resource.lock') do
68
+ ... do something with resource
69
+ end
70
+
71
+ # or
72
+
73
+ mutex = Redis::EM::Mutex.new('resource.lock')
74
+ mutex.synchronize do
75
+ ... do something with resource
76
+ end
77
+
78
+ # or
79
+
80
+ begin
81
+ mutex.lock
82
+ ... do something with resource
83
+ ensure
84
+ mutex.unlock
85
+ end
86
+
87
+ ...
88
+
89
+ Redis::EM::Mutex.stop_watcher
90
+ EM.stop
91
+ end
92
+
93
+ === Namespaces
94
+
95
+ Redis::EM::Mutex.setup(ns: 'my_namespace', ....)
96
+
97
+ # or multiple namespaces:
98
+
99
+ ns = Redis::EM::Mutex::NS.new('my_namespace')
100
+
101
+ EM.synchrony do
102
+ ns.synchronize('foo') do
103
+ .... do something with foo and bar
104
+ end
105
+ ...
106
+ EM.stop
107
+ end
108
+
109
+ === Multi-locks
110
+
111
+ EM.synchrony do
112
+ Redis::EM::Mutex.synchronize('foo', 'bar', 'baz') do
113
+ .... do something with foo, bar and baz
114
+ end
115
+ ...
116
+ EM.stop
117
+ end
118
+
119
+ === Locking options
120
+
121
+ EM.synchrony do
122
+ begin
123
+ Redis::EM::Mutex.synchronize('foo', 'bar', block: 0.25) do
124
+ .... do something with foo and bar
125
+ end
126
+ rescue Redis::EM::Mutex::MutexTimeout
127
+ ... locking timed out
128
+ end
129
+
130
+ Redis::EM::Mutex.synchronize('foo', 'bar', expire: 60) do |mutex|
131
+ .... do something with foo and bar in less than 60 seconds
132
+ if mutex.refresh(120)
133
+ # now we have additional 120 seconds until lock expires
134
+ else
135
+ # too late
136
+ end
137
+ end
138
+
139
+ ...
140
+ EM.stop
141
+ end
142
+
143
+ === Advanced
144
+
145
+ mutex = Redis::EM::Mutex.new('resource1', 'resource2', expire: 60)
146
+
147
+ EM.synchrony do
148
+ mutex.lock
149
+
150
+ EM.fork_reactor do
151
+ Fiber.new do
152
+ mutex.locked? # true
153
+ mutex.owned? # false
154
+ mutex.synchronize do
155
+ mutex.locked? # true
156
+ mutex.owned? # true
157
+
158
+ ....
159
+ end
160
+ ...
161
+ Redis::EM::Mutex.stop_watcher
162
+ EM.stop
163
+ end.resume
164
+ end
165
+
166
+ mutex.locked? # true
167
+ mutex.owned? # true
168
+
169
+ mutex.unlock
170
+ mutex.owned? # false
171
+
172
+ ...
173
+ Redis::EM::Mutex.stop_watcher
174
+ EM.stop
175
+ end
176
+
177
+ == LICENCE
178
+
179
+ The MIT License - Copyright (c) 2012 Rafał Michalski
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ $:.unshift "lib"
2
+
3
+ task :default => [:test]
4
+
5
+ $gem_name = "redis-em-mutex"
6
+
7
+ desc "Run spec tests"
8
+ task :test do
9
+ sh "rspec spec/*.rb"
10
+ end
11
+
12
+ desc "Build the gem"
13
+ task :gem do
14
+ sh "gem build #$gem_name.gemspec"
15
+ end
16
+
17
+ desc "Install the library at local machnie"
18
+ task :install => :gem do
19
+ sh "gem install #$gem_name -l"
20
+ end
21
+
22
+ desc "Uninstall the library from local machnie"
23
+ task :uninstall do
24
+ sh "gem uninstall #$gem_name"
25
+ end
26
+
27
+ desc "Clean"
28
+ task :clean do
29
+ sh "rm #$gem_name*.gem"
30
+ end
31
+
32
+ desc "Documentation"
33
+ task :doc do
34
+ sh "rdoc --encoding=UTF-8 --title=#$gem_name --main=README.rdoc README.rdoc lib/*.rb lib/*/*.rb"
35
+ end
@@ -0,0 +1,5 @@
1
+ class Redis
2
+ module EM
3
+ autoload :Mutex, 'redis/em-mutex'
4
+ end
5
+ end
@@ -0,0 +1,555 @@
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
@@ -0,0 +1,28 @@
1
+ $:.unshift "lib"
2
+ require 'redis/em-mutex'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "redis-em-mutex"
6
+ s.version = Redis::EM::Mutex::VERSION
7
+ s.required_ruby_version = ">= 1.9.1"
8
+ s.date = "#{Time.now.strftime("%Y-%m-%d")}"
9
+ s.summary = "Cross server-process-fiber EventMachine + Redis based semaphore"
10
+ s.email = "rafal@yeondir.com"
11
+ s.homepage = "http://github.com/royaltm/redis-em-mutex"
12
+ s.require_path = "lib"
13
+ s.description = "Cross server-process-fiber EventMachine + Redis based semaphore with many features"
14
+ s.authors = ["Rafal Michalski"]
15
+ s.files = `git ls-files`.split("\n") - ['.gitignore']
16
+ s.test_files = Dir.glob("spec/**/*")
17
+ s.rdoc_options << "--title" << "redis-em-mutex" <<
18
+ "--main" << "README.rdoc"
19
+ s.has_rdoc = true
20
+ s.extra_rdoc_files = ["README.rdoc"]
21
+ s.requirements << "Redis server"
22
+ s.add_runtime_dependency "redis", ">= 3.0.0"
23
+ s.add_runtime_dependency "hiredis", "~> 0.4.5"
24
+ s.add_runtime_dependency "eventmachine", ">= 0.12.10"
25
+ s.add_development_dependency "rspec", "~> 2.8.0"
26
+ s.add_development_dependency "eventmachine", ">= 1.0.0.beta.1"
27
+ s.add_development_dependency "em-synchrony", "~> 1.0.0"
28
+ end
@@ -0,0 +1,33 @@
1
+ $:.unshift "lib"
2
+ require 'em-synchrony'
3
+ require 'redis-em-mutex'
4
+
5
+ describe Redis::EM::Mutex do
6
+
7
+ it "should raise MutexError while redis server not found on setup" do
8
+ expect {
9
+ described_class.setup(host: 'abcdefghijklmnopqrstuvwxyz', reconnect_max: 0)
10
+ }.to raise_error(described_class::MutexError, /Can not establish watcher channel connection!/)
11
+
12
+ expect {
13
+ described_class.setup(host: '255.255.255.255', reconnect_max: 0)
14
+ }.to raise_error(described_class::MutexError, /Can not establish watcher channel connection!/)
15
+
16
+ expect {
17
+ described_class.setup(port: 65535, reconnect_max: 0)
18
+ }.to raise_error(described_class::MutexError, /Can not establish watcher channel connection!/)
19
+ end
20
+
21
+ around(:each) do |testcase|
22
+ @after_em_stop = nil
23
+ ::EM.synchrony do
24
+ begin
25
+ testcase.call
26
+ ensure
27
+ ::EM.stop
28
+ end
29
+ end
30
+ @after_em_stop.call if @after_em_stop
31
+ end
32
+
33
+ end
@@ -0,0 +1,337 @@
1
+ $:.unshift "lib"
2
+ require 'securerandom'
3
+ require 'em-synchrony'
4
+ require 'em-synchrony/fiber_iterator'
5
+ require 'redis-em-mutex'
6
+
7
+ describe Redis::EM::Mutex do
8
+
9
+ it "should lock and prevent locking on the same semaphore" do
10
+ begin
11
+ described_class.new(@lock_names.first).owned?.should be_false
12
+ mutex = described_class.lock(@lock_names.first)
13
+ mutex.names.should eq [@lock_names.first]
14
+ mutex.locked?.should be_true
15
+ mutex.owned?.should be_true
16
+ mutex.should be_an_instance_of described_class
17
+ described_class.new(@lock_names.first).try_lock.should be_false
18
+ expect {
19
+ mutex.lock
20
+ }.to raise_error(Redis::EM::Mutex::MutexError, /deadlock; recursive locking/)
21
+ mutex.unlock.should be_an_instance_of described_class
22
+ mutex.locked?.should be_false
23
+ mutex.owned?.should be_false
24
+ mutex.try_lock.should be_true
25
+ ensure
26
+ mutex.unlock if mutex
27
+ end
28
+ end
29
+
30
+ it "should lock and prevent locking on the same multiple semaphores" do
31
+ begin
32
+ mutex = described_class.lock(*@lock_names)
33
+ mutex.names.should eq @lock_names
34
+ mutex.locked?.should be_true
35
+ mutex.owned?.should be_true
36
+ mutex.should be_an_instance_of described_class
37
+ described_class.new(*@lock_names).try_lock.should be_false
38
+ @lock_names.each do |name|
39
+ described_class.new(name).try_lock.should be_false
40
+ end
41
+ mutex.try_lock.should be_false
42
+ expect {
43
+ mutex.lock
44
+ }.to raise_error(Redis::EM::Mutex::MutexError, /deadlock; recursive locking/)
45
+ @lock_names.each do |name|
46
+ expect {
47
+ described_class.new(name).lock
48
+ }.to raise_error(Redis::EM::Mutex::MutexError, /deadlock; recursive locking/)
49
+ end
50
+ mutex.unlock.should be_an_instance_of described_class
51
+ mutex.locked?.should be_false
52
+ mutex.owned?.should be_false
53
+ mutex.try_lock.should be_true
54
+ ensure
55
+ mutex.unlock if mutex
56
+ end
57
+ end
58
+
59
+ it "should lock and prevent other fibers to lock on the same semaphore" do
60
+ begin
61
+ mutex = described_class.lock(@lock_names.first)
62
+ mutex.should be_an_instance_of described_class
63
+ mutex.owned?.should be_true
64
+ locked = true
65
+ ::EM::Synchrony.next_tick do
66
+ mutex.try_lock.should be false
67
+ mutex.owned?.should be_false
68
+ start = Time.now
69
+ mutex.synchronize do
70
+ (Time.now - start).should be_within(0.01).of(0.26)
71
+ locked.should be false
72
+ locked = nil
73
+ end
74
+ end
75
+ ::EM::Synchrony.sleep 0.25
76
+ locked = false
77
+ mutex.owned?.should be_true
78
+ mutex.unlock.should be_an_instance_of described_class
79
+ mutex.owned?.should be_false
80
+ ::EM::Synchrony.sleep 0.1
81
+ locked.should be_nil
82
+ ensure
83
+ mutex.unlock if mutex
84
+ end
85
+ end
86
+
87
+ it "should lock and prevent other fibers to lock on the same multiple semaphores" do
88
+ begin
89
+ mutex = described_class.lock(*@lock_names)
90
+ mutex.should be_an_instance_of described_class
91
+ mutex.owned?.should be_true
92
+ locked = true
93
+ ::EM::Synchrony.next_tick do
94
+ locked.should be true
95
+ mutex.try_lock.should be false
96
+ mutex.owned?.should be_false
97
+ start = Time.now
98
+ mutex.synchronize do
99
+ mutex.owned?.should be_true
100
+ (Time.now - start).should be_within(0.01).of(0.26)
101
+ locked.should be false
102
+ end
103
+ mutex.owned?.should be_false
104
+ ::EM::Synchrony.sleep 0.1
105
+ start = Time.now
106
+ ::EM::Synchrony::FiberIterator.new(@lock_names, @lock_names.length).each do |name|
107
+ locked.should be true
108
+ described_class.new(name).synchronize do
109
+ (Time.now - start).should be_within(0.01).of(0.26)
110
+ locked.should be_an_instance_of Fixnum
111
+ locked-= 1
112
+ end
113
+ end
114
+ end
115
+ ::EM::Synchrony.sleep 0.25
116
+ locked = false
117
+ mutex.owned?.should be_true
118
+ mutex.unlock.should be_an_instance_of described_class
119
+ mutex.owned?.should be_false
120
+ ::EM::Synchrony.sleep 0.1
121
+
122
+ locked = true
123
+ mutex.lock.should be true
124
+ ::EM::Synchrony.sleep 0.25
125
+ locked = 10
126
+ mutex.unlock.should be_an_instance_of described_class
127
+ ::EM::Synchrony.sleep 0.1
128
+ locked.should eq 0
129
+ ensure
130
+ mutex.unlock if mutex
131
+ end
132
+ end
133
+
134
+ it "should lock and prevent other fibers to lock on the same semaphore with block timeout" do
135
+ begin
136
+ mutex = described_class.lock(*@lock_names)
137
+ mutex.should be_an_instance_of described_class
138
+ mutex.owned?.should be_true
139
+ locked = true
140
+ ::EM::Synchrony.next_tick do
141
+ start = Time.now
142
+ mutex.lock(0.25).should be false
143
+ mutex.owned?.should be_false
144
+ (Time.now - start).should be_within(0.01).of(0.26)
145
+ locked.should be true
146
+ locked = nil
147
+ end
148
+ ::EM::Synchrony.sleep 0.26
149
+ locked.should be_nil
150
+ locked = false
151
+ mutex.locked?.should be_true
152
+ mutex.owned?.should be_true
153
+ mutex.unlock.should be_an_instance_of described_class
154
+ mutex.locked?.should be_false
155
+ mutex.owned?.should be_false
156
+ ensure
157
+ mutex.unlock if mutex
158
+ end
159
+ end
160
+
161
+ it "should lock and expire while other fiber lock on the same semaphore with block timeout" do
162
+ begin
163
+ mutex = described_class.lock(*@lock_names, expire: 0.2499999)
164
+ mutex.expire_timeout.should eq 0.2499999
165
+ mutex.should be_an_instance_of described_class
166
+ mutex.owned?.should be_true
167
+ locked = true
168
+ ::EM::Synchrony.next_tick do
169
+ mutex.owned?.should be_false
170
+ start = Time.now
171
+ mutex.lock(0.25).should be true
172
+ (Time.now - start).should be_within(0.011).of(0.26)
173
+ locked.should be true
174
+ locked = nil
175
+ mutex.locked?.should be_true
176
+ mutex.owned?.should be_true
177
+ ::EM::Synchrony.sleep 0.2
178
+ locked.should be_false
179
+ mutex.unlock.should be_an_instance_of described_class
180
+ mutex.owned?.should be_false
181
+ mutex.locked?.should be_false
182
+ end
183
+ ::EM::Synchrony.sleep 0.26
184
+ locked.should be_nil
185
+ locked = false
186
+ mutex.locked?.should be_true
187
+ mutex.owned?.should be_false
188
+ mutex.unlock.should be_an_instance_of described_class
189
+ mutex.locked?.should be_true
190
+ mutex.owned?.should be_false
191
+ ::EM::Synchrony.sleep 0.2
192
+ ensure
193
+ mutex.unlock if mutex
194
+ end
195
+ end
196
+
197
+ it "should lock and prevent (with refresh) other fibers to lock on the same semaphore with block timeout" do
198
+ begin
199
+ mutex = described_class.lock(*@lock_names, expire: 0.11)
200
+ mutex.should be_an_instance_of described_class
201
+ mutex.owned?.should be_true
202
+ locked = true
203
+ ::EM::Synchrony.next_tick do
204
+ start = Time.now
205
+ mutex.lock(0.3).should be false
206
+ mutex.owned?.should be_false
207
+ (Time.now - start).should be_within(0.01).of(0.31)
208
+ locked.should be true
209
+ locked = nil
210
+ end
211
+ ::EM::Synchrony.sleep 0.08
212
+ mutex.owned?.should be_true
213
+ mutex.refresh
214
+ ::EM::Synchrony.sleep 0.08
215
+ mutex.owned?.should be_true
216
+ mutex.refresh(0.5)
217
+ ::EM::Synchrony.sleep 0.15
218
+ locked.should be_nil
219
+ locked = false
220
+ mutex.locked?.should be_true
221
+ mutex.owned?.should be_true
222
+ mutex.unlock.should be_an_instance_of described_class
223
+ mutex.locked?.should be_false
224
+ mutex.owned?.should be_false
225
+ ensure
226
+ mutex.unlock if mutex
227
+ end
228
+ end
229
+
230
+ it "should lock some resource and play with it safely" do
231
+ mutex = described_class.new(*@lock_names)
232
+ play_name = SecureRandom.random_bytes
233
+ result = []
234
+ ::EM::Synchrony::FiberIterator.new((0..9).to_a, 10).each do |i|
235
+ was_locked = false
236
+ redis = Redis.new @redis_options
237
+ mutex.owned?.should be_false
238
+ mutex.synchronize do
239
+ mutex.owned?.should be_true
240
+ was_locked = true
241
+ redis.setnx(play_name, i).should be_true
242
+ ::EM::Synchrony.sleep 0.1
243
+ redis.get(play_name).should eq i.to_s
244
+ redis.del(play_name).should eq 1
245
+ end
246
+ was_locked.should be_true
247
+ mutex.owned?.should be_false
248
+ result << i
249
+ end
250
+ mutex.locked?.should be_false
251
+ result.sort.should eq (0..9).to_a
252
+ end
253
+
254
+ it "should lock and the other fiber should acquire lock as soon as possible" do
255
+ mutex = described_class.lock(*@lock_names)
256
+ mutex.should be_an_instance_of described_class
257
+ time = nil
258
+ EM::Synchrony.next_tick do
259
+ time.should be_nil
260
+ was_locked = false
261
+ mutex.synchronize do
262
+ time.should be_an_instance_of Time
263
+ (Time.now - time).should be < 0.0009
264
+ was_locked = true
265
+ end
266
+ was_locked.should be_true
267
+ end
268
+ EM::Synchrony.sleep 0.1
269
+ mutex.owned?.should be_true
270
+ mutex.unlock.should be_an_instance_of described_class
271
+ time = Time.now
272
+ mutex.owned?.should be_false
273
+ EM::Synchrony.sleep 0.1
274
+ end
275
+
276
+ it "should lock and the other process should acquire lock as soon as possible" do
277
+ mutex = described_class.lock(*@lock_names)
278
+ mutex.should be_an_instance_of described_class
279
+ time_key1 = SecureRandom.random_bytes
280
+ time_key2 = SecureRandom.random_bytes
281
+ ::EM.fork_reactor do
282
+ Fiber.new do
283
+ begin
284
+ redis = Redis.new @redis_options
285
+ redis.set time_key1, Time.now.to_f.to_s
286
+ mutex.synchronize do
287
+ redis.set time_key2, Time.now.to_f.to_s
288
+ end
289
+ described_class.stop_watcher(false)
290
+ # rescue => e
291
+ # warn e.inspect
292
+ ensure
293
+ EM.stop
294
+ end
295
+ end.resume
296
+ end
297
+ EM::Synchrony.sleep 0.25
298
+ mutex.owned?.should be_true
299
+ mutex.unlock.should be_an_instance_of described_class
300
+ time = Time.now.to_f
301
+ mutex.owned?.should be_false
302
+ EM::Synchrony.sleep 0.25
303
+ redis = Redis.new @redis_options
304
+ t1, t2 = redis.mget(time_key1, time_key2)
305
+ t1.should be_an_instance_of String
306
+ t1.to_f.should be < time - 0.25
307
+ t2.should be_an_instance_of String
308
+ t2.to_f.should be > time
309
+ t2.to_f.should be_within(0.001).of(time)
310
+ redis.del(time_key1, time_key2)
311
+ end
312
+
313
+ around(:each) do |testcase|
314
+ @after_em_stop = nil
315
+ ::EM.synchrony do
316
+ begin
317
+ testcase.call
318
+ ensure
319
+ described_class.stop_watcher(false)
320
+ ::EM.stop
321
+ end
322
+ end
323
+ @after_em_stop.call if @after_em_stop
324
+ end
325
+
326
+ before(:all) do
327
+ @redis_options = {}
328
+ described_class.setup @redis_options.merge(size: 11)
329
+ @lock_names = 10.times.map {
330
+ SecureRandom.random_bytes
331
+ }
332
+ end
333
+
334
+ after(:all) do
335
+ # @lock_names
336
+ end
337
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-em-mutex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rafal Michalski
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &204598440 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *204598440
25
+ - !ruby/object:Gem::Dependency
26
+ name: hiredis
27
+ requirement: &204597980 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.4.5
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *204597980
36
+ - !ruby/object:Gem::Dependency
37
+ name: eventmachine
38
+ requirement: &204597520 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 0.12.10
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *204597520
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &204597060 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.8.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *204597060
58
+ - !ruby/object:Gem::Dependency
59
+ name: eventmachine
60
+ requirement: &204596600 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: 1.0.0.beta.1
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *204596600
69
+ - !ruby/object:Gem::Dependency
70
+ name: em-synchrony
71
+ requirement: &204596140 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: 1.0.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *204596140
80
+ description: Cross server-process-fiber EventMachine + Redis based semaphore with
81
+ many features
82
+ email: rafal@yeondir.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files:
86
+ - README.rdoc
87
+ files:
88
+ - README.rdoc
89
+ - Rakefile
90
+ - lib/redis-em-mutex.rb
91
+ - lib/redis/em-mutex.rb
92
+ - redis-em-mutex.gemspec
93
+ - spec/redis-em-mutex-features.rb
94
+ - spec/redis-em-mutex-semaphores.rb
95
+ homepage: http://github.com/royaltm/redis-em-mutex
96
+ licenses: []
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --title
100
+ - redis-em-mutex
101
+ - --main
102
+ - README.rdoc
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: 1.9.1
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements:
118
+ - Redis server
119
+ rubyforge_project:
120
+ rubygems_version: 1.8.17
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: Cross server-process-fiber EventMachine + Redis based semaphore
124
+ test_files:
125
+ - spec/redis-em-mutex-semaphores.rb
126
+ - spec/redis-em-mutex-features.rb