redis-mutex 2.1.0 → 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 938cf6fdd7b6baa536703e89f2f45dbdd7a7725ee2a8595e840515c8317dfac9
4
+ data.tar.gz: 172a141c6d8218384de2a45b861ff7ed2a85fbe4a8cd52d60a6b2c7a3bca6c76
5
+ SHA512:
6
+ metadata.gz: 198b9c51d0f26c282d88f8d3d3555f283e3d5b57080c0a75788243359440a791c896ba32dca09b4f5c43f9a6d9a61eaff9621412ac9abb474a31b20aa17ff44e
7
+ data.tar.gz: 61009d9d9bb9c50697f48041f0535a366141234b9dab5480c500f9b00eb2f526ee92c1ee0d923adfe8192fb0dadf430714806627bcc948ee357fbc8130028c24
@@ -1,12 +1,12 @@
1
1
  language: ruby
2
+ services:
3
+ - redis-server
4
+ before_install:
5
+ - gem install bundler
6
+ - gem update bundler
2
7
  rvm:
3
- - 1.8.7
4
- - 1.9.2
5
- - 1.9.3
6
- - jruby-18mode
7
- - jruby-19mode
8
- - rbx-18mode
9
- - rbx-19mode
8
+ - 2.1
9
+ - 2.2
10
+ - 2.3.0
11
+ - 2.7.0
10
12
  - ruby-head
11
- - jruby-head
12
- - ree
data/README.md CHANGED
@@ -3,7 +3,7 @@ Redis Mutex
3
3
 
4
4
  [![Build Status](https://secure.travis-ci.org/kenn/redis-mutex.png)](http://travis-ci.org/kenn/redis-mutex)
5
5
 
6
- Distrubuted mutex in Ruby using Redis. Supports both **blocking** and **non-blocking** semantics.
6
+ Distributed mutex in Ruby using Redis. Supports both **blocking** and **non-blocking** semantics.
7
7
 
8
8
  The idea was taken from [the official SETNX doc](http://redis.io/commands/setnx).
9
9
 
@@ -13,7 +13,7 @@ Synopsis
13
13
  In the following example, only one thread / process / server can enter the locked block at one time.
14
14
 
15
15
  ```ruby
16
- Redis::Mutex.with_lock(:your_lock_name) do
16
+ RedisMutex.with_lock(:your_lock_name) do
17
17
  # do something exclusively
18
18
  end
19
19
  ```
@@ -21,7 +21,7 @@ end
21
21
  or
22
22
 
23
23
  ```ruby
24
- mutex = Redis::Mutex.new(:your_lock_name)
24
+ mutex = RedisMutex.new(:your_lock_name)
25
25
  if mutex.lock
26
26
  # do something exclusively
27
27
  mutex.unlock
@@ -37,10 +37,24 @@ that you can configure any of these timing values, as explained later.
37
37
 
38
38
  Or if you want to immediately receive `false` on an unsuccessful locking attempt, you can change the mutex mode to **non-blocking**.
39
39
 
40
- Changes in v2.0
41
- ---------------
40
+ Changelog
41
+ ---------
42
42
 
43
- * **Exception-based control flow**: Added `lock!` and `unlock!`, which raises an exception when fails to acquire a lock. Raises `Redis::Mutex::LockError` and `Redis::Mutex::UnlockError` respectively.
43
+ ### v4.0
44
+
45
+ `redis-mutex` 4.0 has brought a few backward incompatible changes to follow the major upgrade of the underlying `redis-classy` gem.
46
+
47
+ * The base class `Redis::Mutex` is now `RedisMutex`.
48
+ * `Redis::Classy.db = Redis.new` is now `RedisClassy.redis = Redis.new`.
49
+
50
+ ### v3.0
51
+
52
+ * Ruby 2.0 or later is required.
53
+ * `auto_mutex` now takes `:on` for additional key scoping.
54
+
55
+ ### v2.0
56
+
57
+ * **Exception-based control flow**: Added `lock!` and `unlock!`, which raises an exception when fails to acquire a lock. Raises `RedisMutex::LockError` and `RedisMutex::UnlockError` respectively.
44
58
  * **INCOMPATIBLE CHANGE**: `#lock` no longer accepts a block. Use `#with_lock` instead, which uses `lock!` internally and returns the value of block.
45
59
  * `unlock` returns boolean values for success / failure, for consistency with `lock`.
46
60
 
@@ -61,7 +75,7 @@ gem 'redis-mutex'
61
75
  Register the Redis server: (e.g. in `config/initializers/redis_mutex.rb` for Rails)
62
76
 
63
77
  ```ruby
64
- Redis::Classy.db = Redis.new(:host => 'localhost')
78
+ RedisClassy.redis = Redis.new
65
79
  ```
66
80
 
67
81
  Note that Redis Mutex uses the `redis-classy` gem internally to organize keys in an isolated namespace.
@@ -69,7 +83,7 @@ Note that Redis Mutex uses the `redis-classy` gem internally to organize keys in
69
83
  There are a number of methods:
70
84
 
71
85
  ```ruby
72
- mutex = Redis::Mutex.new(key, options) # Configure a mutex lock
86
+ mutex = RedisMutex.new(key, options) # Configure a mutex lock
73
87
  mutex.lock # Try to acquire the lock, returns false when failed
74
88
  mutex.lock! # Try to acquire the lock, raises exception when failed
75
89
  mutex.unlock # Try to release the lock, returns false when failed
@@ -78,20 +92,20 @@ mutex.locked? # Find out if resource already locked
78
92
  mutex.with_lock # Try to acquire the lock, execute the block, then return the value of the block.
79
93
  # Raises exception when failed to acquire the lock.
80
94
 
81
- Redis::Mutex.sweep # Remove all expired locks
82
- Redis::Mutex.with_lock(key, options) # Shortcut to new + with_lock
95
+ RedisMutex.sweep # Remove all expired locks
96
+ RedisMutex.with_lock(key, options) # Shortcut to new + with_lock
83
97
  ```
84
98
 
85
99
  The key argument can be symbol, string, or any Ruby objects that respond to `id` method, where the key is automatically set as
86
- `TheClass:id`. For any given key, `Redis::Mutex:` prefix will be automatically prepended. For instance, if you pass a `Room`
87
- object with id of `123`, the actual key in Redis will be `Redis::Mutex:Room:123`. The automatic prefixing and instance binding
88
- is the feature of `Redis::Classy` - for more internal details, refer to [Redis Classy](https://github.com/kenn/redis-classy).
100
+ `TheClass:id`. For any given key, `RedisMutex:` prefix will be automatically prepended. For instance, if you pass a `Room`
101
+ object with id of `123`, the actual key in Redis will be `RedisMutex:Room:123`. The automatic prefixing and instance binding
102
+ is the feature of `RedisClassy` - for more internal details, refer to [Redis Classy](https://github.com/kenn/redis-classy).
89
103
 
90
104
  The initialize method takes several options.
91
105
 
92
106
  ```ruby
93
107
  :block => 1 # Specify in seconds how long you want to wait for the lock to be released.
94
- # Speficy 0 if you need non-blocking sematics and return false immediately. (default: 1)
108
+ # Specify 0 if you need non-blocking sematics and return false immediately. (default: 1)
95
109
  :sleep => 0.1 # Specify in seconds how long the polling interval should be when :block is given.
96
110
  # It is NOT recommended to go below 0.01. (default: 0.1)
97
111
  :expire => 10 # Specify in seconds when the lock should be considered stale when something went wrong
@@ -108,11 +122,11 @@ class RoomController < ApplicationController
108
122
  before_filter { @room = Room.find(params[:id]) }
109
123
 
110
124
  def enter
111
- Redis::Mutex.with_lock(@room) do # key => "Room:123"
125
+ RedisMutex.with_lock(@room) do # key => "Room:123"
112
126
  # do something exclusively
113
127
  end
114
128
  render text: 'success!'
115
- rescue Redis::Mutex::LockError
129
+ rescue RedisMutex::LockError
116
130
  render text: 'failed to acquire lock!'
117
131
  end
118
132
  end
@@ -123,7 +137,7 @@ put the `unlock` method in the `ensure` clause.
123
137
 
124
138
  ```ruby
125
139
  def enter
126
- mutex = Redis::Mutex.new('non-blocking', block: 0, expire: 10.minutes)
140
+ mutex = RedisMutex.new('non-blocking', block: 0, expire: 10.minutes)
127
141
  if mutex.lock
128
142
  begin
129
143
  # do something exclusively
@@ -147,7 +161,7 @@ If you give a proc object to the `after_failure` option, it will get called afte
147
161
 
148
162
  ```ruby
149
163
  class JobController < ApplicationController
150
- include Redis::Mutex::Macro
164
+ include RedisMutex::Macro
151
165
  auto_mutex :run, block: 0, after_failure: lambda { render text: 'failed to acquire lock!' }
152
166
 
153
167
  def run
@@ -156,3 +170,17 @@ class JobController < ApplicationController
156
170
  end
157
171
  end
158
172
  ```
173
+
174
+ Also you can specify method arguments with the `on` option. The following creates a mutex key named `ItunesVerifier#perform:123456`, so that the same method can run in parallel as long as the `transaction_id` is different.
175
+
176
+ ```ruby
177
+ class ItunesVerifier
178
+ include Sidekiq::Worker
179
+ include RedisMutex::Macro
180
+ auto_mutex :perform, on: [:transaction_id]
181
+
182
+ def perform(transaction_id)
183
+ ...
184
+ end
185
+ end
186
+ ```
@@ -1,5 +1,2 @@
1
1
  require 'redis-classy'
2
-
3
- class Redis
4
- autoload :Mutex, 'redis/mutex'
5
- end
2
+ require 'redis_mutex'
@@ -0,0 +1,132 @@
1
+ class RedisMutex < RedisClassy
2
+ #
3
+ # Options
4
+ #
5
+ # :block => Specify in seconds how long you want to wait for the lock to be released. Speficy 0
6
+ # if you need non-blocking sematics and return false immediately. (default: 1)
7
+ # :sleep => Specify in seconds how long the polling interval should be when :block is given.
8
+ # It is recommended that you do NOT go below 0.01. (default: 0.1)
9
+ # :expire => Specify in seconds when the lock should forcibly be removed when something went wrong
10
+ # with the one who held the lock. (default: 10)
11
+ #
12
+ autoload :Macro, 'redis_mutex/macro'
13
+
14
+ DEFAULT_EXPIRE = 10
15
+ LockError = Class.new(StandardError)
16
+ UnlockError = Class.new(StandardError)
17
+ AssertionError = Class.new(StandardError)
18
+
19
+ def initialize(object, options={})
20
+ super(object.is_a?(String) || object.is_a?(Symbol) ? object : "#{object.class.name}:#{object.id}")
21
+ @block = options[:block] || 1
22
+ @sleep = options[:sleep] || 0.1
23
+ @expire = options[:expire] || DEFAULT_EXPIRE
24
+ end
25
+
26
+ def lock
27
+ self.class.raise_assertion_error if block_given?
28
+ @locking = false
29
+
30
+ if @block > 0
31
+ # Blocking mode
32
+ start_at = Time.now
33
+ while Time.now - start_at < @block
34
+ @locking = true and break if try_lock
35
+ sleep @sleep
36
+ end
37
+ else
38
+ # Non-blocking mode
39
+ @locking = try_lock
40
+ end
41
+ @locking
42
+ end
43
+
44
+ def try_lock
45
+ now = Time.now.to_f
46
+ @expires_at = now + @expire # Extend in each blocking loop
47
+
48
+ begin
49
+ return true if setnx(@expires_at) # Success, the lock has been acquired
50
+ end until old_value = get # Repeat if unlocked before get
51
+
52
+ return false if old_value.to_f > now # Check if the lock is still effective
53
+
54
+ # The lock has expired but wasn't released... BAD!
55
+ return true if getset(@expires_at).to_f <= now # Success, we acquired the previously expired lock
56
+ return false # Dammit, it seems that someone else was even faster than us to remove the expired lock!
57
+ end
58
+
59
+ # Returns true if resource is locked. Note that nil.to_f returns 0.0
60
+ def locked?
61
+ get.to_f > Time.now.to_f
62
+ end
63
+
64
+ def unlock(force = false)
65
+ # Since it's possible that the operations in the critical section took a long time,
66
+ # we can't just simply release the lock. The unlock method checks if @expires_at
67
+ # remains the same, and do not release when the lock timestamp was overwritten.
68
+
69
+ if get == @expires_at.to_s or force
70
+ # Redis#del with a single key returns '1' or nil
71
+ !!del
72
+ else
73
+ false
74
+ end
75
+ end
76
+
77
+ def with_lock
78
+ if lock!
79
+ begin
80
+ @result = yield
81
+ ensure
82
+ unlock
83
+ end
84
+ end
85
+ @result
86
+ end
87
+
88
+ def lock!
89
+ lock or raise LockError, "failed to acquire lock #{key.inspect}"
90
+ end
91
+
92
+ def unlock!(force = false)
93
+ unlock(force) or raise UnlockError, "failed to release lock #{key.inspect}"
94
+ end
95
+
96
+ class << self
97
+ def sweep
98
+ return 0 if (all_keys = keys).empty?
99
+
100
+ now = Time.now.to_f
101
+ values = mget(*all_keys)
102
+
103
+ expired_keys = all_keys.zip(values).select do |key, time|
104
+ time && time.to_f <= now
105
+ end
106
+
107
+ expired_keys.each do |key, _|
108
+ # Make extra sure that anyone haven't extended the lock
109
+ del(key) if getset(key, now + DEFAULT_EXPIRE).to_f <= now
110
+ end
111
+
112
+ expired_keys.size
113
+ end
114
+
115
+ def lock(object, options = {})
116
+ raise_assertion_error if block_given?
117
+ new(object, options).lock
118
+ end
119
+
120
+ def lock!(object, options = {})
121
+ new(object, options).lock!
122
+ end
123
+
124
+ def with_lock(object, options = {}, &block)
125
+ new(object, options).with_lock(&block)
126
+ end
127
+
128
+ def raise_assertion_error
129
+ raise AssertionError, 'block syntax has been removed from #lock, use #with_lock instead'
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,61 @@
1
+ class RedisMutex
2
+ module Macro
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.class_eval do
6
+ class << self
7
+ attr_accessor :auto_mutex_methods
8
+ end
9
+ @auto_mutex_methods = {}
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def auto_mutex(target, options={})
15
+ self.auto_mutex_methods[target] = options
16
+ end
17
+
18
+ def method_added(target)
19
+ return if target.to_s =~ /^auto_mutex_methods/
20
+ return unless self.auto_mutex_methods[target]
21
+ without_method = "#{target}_without_auto_mutex"
22
+ with_method = "#{target}_with_auto_mutex"
23
+ after_method = "#{target}_after_failure"
24
+ return if method_defined?(without_method)
25
+ options = self.auto_mutex_methods[target]
26
+
27
+ if options[:after_failure].is_a?(Proc)
28
+ define_method(after_method, &options[:after_failure])
29
+ end
30
+ target_argument_names = instance_method(target.to_sym).parameters.map(&:last)
31
+ on_arguments = Array(options[:on])
32
+ mutex_arguments = on_arguments & target_argument_names
33
+ unknown_arguments = on_arguments - target_argument_names
34
+ if unknown_arguments.any?
35
+ raise ArgumentError, "You are trying to lock on unknown arguments: #{unknown_arguments.join(', ')}"
36
+ end
37
+
38
+ define_method(with_method) do |*args|
39
+ named_arguments = Hash[target_argument_names.zip(args)]
40
+ arguments = mutex_arguments.map { |name| named_arguments.fetch(name) }
41
+ key = format(
42
+ "%<class>s#%<target>s:%<arguments>s",
43
+ class: self.class.name,
44
+ target: target,
45
+ arguments: arguments.join(":")
46
+ )
47
+ begin
48
+ RedisMutex.with_lock(key, options) do
49
+ send(without_method, *args)
50
+ end
51
+ rescue RedisMutex::LockError
52
+ send(after_method, *args) if respond_to?(after_method)
53
+ end
54
+ end
55
+
56
+ alias_method without_method, target
57
+ alias_method target, with_method
58
+ end
59
+ end
60
+ end
61
+ end
@@ -12,9 +12,9 @@ Gem::Specification.new do |gem|
12
12
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
13
  gem.name = "redis-mutex"
14
14
  gem.require_paths = ["lib"]
15
- gem.version = '2.1.0' # retrieve this value by: Gem.loaded_specs['redis-mutex'].version.to_s
15
+ gem.version = '4.0.2' # retrieve this value by: Gem.loaded_specs['redis-mutex'].version.to_s
16
16
 
17
- gem.add_runtime_dependency "redis-classy", "~> 1.2"
17
+ gem.add_runtime_dependency "redis-classy", "~> 2.0"
18
18
  gem.add_development_dependency "rspec"
19
19
  gem.add_development_dependency "bundler"
20
20
 
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ class C
4
+ include RedisMutex::Macro
5
+ auto_mutex :run_singularly, :block => 0, :after_failure => lambda {|id| return "failure: #{id}" }
6
+
7
+ def run_singularly(id)
8
+ sleep 0.1
9
+ return "success: #{id}"
10
+ end
11
+
12
+ auto_mutex :run_singularly_on_args, :block => 0, :on => [:id, :bar], :after_failure => lambda {|id, *others| return "failure: #{id}" }
13
+ def run_singularly_on_args(id, foo, bar)
14
+ sleep 0.1
15
+ return "success: #{id}"
16
+ end
17
+
18
+ auto_mutex :run_singularly_on_keyword_args, :block => 0, :on => [:id, :bar], :after_failure => lambda {|id: 1, **others| return "failure: #{id}" }
19
+ def run_singularly_on_keyword_args(id: 1, foo: 1, bar: 1)
20
+ sleep 0.1
21
+ return "success: #{id}"
22
+ end
23
+ end
24
+
25
+ describe RedisMutex::Macro do
26
+
27
+ def race(a, b)
28
+ t1 = Thread.new(&a)
29
+ # In most cases t1 wins, but make sure to give it a head start,
30
+ # not exceeding the sleep inside the method.
31
+ sleep 0.01
32
+ t2 = Thread.new(&b)
33
+ t1.join
34
+ t2.join
35
+ end
36
+
37
+ let(:object_arg) { Object.new }
38
+
39
+ it 'adds auto_mutex' do
40
+ race(
41
+ proc { expect(C.new.run_singularly(1)).to eq("success: 1") },
42
+ proc { expect(C.new.run_singularly(2)).to eq("failure: 2") })
43
+ end
44
+
45
+ it 'adds auto_mutex on different args' do
46
+ race(
47
+ proc { expect(C.new.run_singularly_on_args(1, :'2', object_arg)).to eq("success: 1") },
48
+ proc { expect(C.new.run_singularly_on_args(2, :'2', object_arg)).to eq("success: 2") })
49
+ end
50
+
51
+ it 'adds auto_mutex on same args' do
52
+ race(
53
+ proc { expect(C.new.run_singularly_on_args(1, :'2', object_arg)).to eq("success: 1") },
54
+ proc { expect(C.new.run_singularly_on_args(1, :'2', object_arg)).to eq("failure: 1") })
55
+ end
56
+
57
+ it 'adds auto_mutex on different keyword args' do
58
+ race(
59
+ proc { expect(C.new.run_singularly_on_keyword_args(id: 1, foo: :'2', bar: object_arg)).to eq("success: 1") },
60
+ proc { expect(C.new.run_singularly_on_keyword_args(id: 2, foo: :'2', bar: object_arg)).to eq("success: 2") })
61
+ end
62
+
63
+ it 'adds auto_mutex on same keyword args' do
64
+ race(
65
+ proc { expect(C.new.run_singularly_on_keyword_args(id: 1, foo: :'2', bar: object_arg)).to eq("success: 1") },
66
+ proc { expect(C.new.run_singularly_on_keyword_args(id: 1, foo: :'2', bar: object_arg)).to eq("failure: 1") })
67
+ end
68
+
69
+ it 'raise exception if there is no such argument' do
70
+ expect {
71
+ class C
72
+ auto_mutex :run_without_such_args, :block => 0, :on => [:missing_arg]
73
+ def run_without_such_args(id)
74
+ return "success: #{id}"
75
+ end
76
+ end
77
+ }.to raise_error(ArgumentError) { |error|
78
+ expect(error.message).to eq 'You are trying to lock on unknown arguments: missing_arg'
79
+ }
80
+ end
81
+ end
@@ -2,173 +2,192 @@ require 'spec_helper'
2
2
 
3
3
  SHORT_MUTEX_OPTIONS = { :block => 0.1, :sleep => 0.02 }
4
4
 
5
- class C
6
- include Redis::Mutex::Macro
7
- auto_mutex :run_singularly, :block => 0, :after_failure => lambda {|id| return "failure: #{id}" }
8
-
9
- def run_singularly(id)
10
- sleep 0.1
11
- return "success: #{id}"
12
- end
13
- end
14
-
15
- describe Redis::Mutex do
5
+ describe RedisMutex do
16
6
  before do
17
- Redis::Classy.flushdb
7
+ RedisClassy.flushdb
18
8
  end
19
9
 
20
10
  after :all do
21
- Redis::Classy.flushdb
22
- Redis::Classy.quit
11
+ RedisClassy.flushdb
12
+ RedisClassy.quit
23
13
  end
24
14
 
25
15
  it 'locks the universe' do
26
- mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
27
- mutex1.lock.should be_true
16
+ mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
17
+ expect(mutex1.lock).to be_truthy
28
18
 
29
- mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
30
- mutex2.lock.should be_false
19
+ mutex2 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
20
+ expect(mutex2.lock).to be_falsey
31
21
  end
32
22
 
33
23
  it 'fails to lock when the lock is taken' do
34
- mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
24
+ mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
35
25
 
36
- mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
37
- mutex2.lock.should be_true # mutex2 beats us to it
26
+ mutex2 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
27
+ expect(mutex2.lock).to be_truthy # mutex2 beats us to it
38
28
 
39
- mutex1.lock.should be_false # fail
29
+ expect(mutex1.lock).to be_falsey # fail
40
30
  end
41
31
 
42
32
  it 'unlocks only once' do
43
- mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
44
- mutex.lock.should be_true
33
+ mutex = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
34
+ expect(mutex.lock).to be_truthy
45
35
 
46
- mutex.unlock.should be_true # successfully released the lock
47
- mutex.unlock.should be_false # the lock no longer exists
36
+ expect(mutex.unlock).to be_truthy # successfully released the lock
37
+ expect(mutex.unlock).to be_falsey # the lock no longer exists
48
38
  end
49
39
 
50
40
  it 'prevents accidental unlock from outside' do
51
- mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
52
- mutex1.lock.should be_true
41
+ mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
42
+ expect(mutex1.lock).to be_truthy
53
43
 
54
- mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
55
- mutex2.unlock.should be_false
44
+ mutex2 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
45
+ expect(mutex2.unlock).to be_falsey
56
46
  end
57
47
 
58
48
  it 'sets expiration' do
59
49
  start = Time.now
60
50
  expires_in = 10
61
- mutex = Redis::Mutex.new(:test_lock, :expire => expires_in)
51
+ mutex = RedisMutex.new(:test_lock, :expire => expires_in)
62
52
  mutex.with_lock do
63
- mutex.get.to_f.should be_within(1.0).of((start + expires_in).to_f)
53
+ expect(mutex.get.to_f).to be_within(1.0).of((start + expires_in).to_f)
64
54
  end
65
- mutex.get.should be_nil # key should have been cleaned up
55
+ expect(mutex.get).to be_nil # key should have been cleaned up
66
56
  end
67
57
 
68
58
  it 'overwrites a lock when existing lock is expired' do
69
59
  # stale lock from the far past
70
- Redis::Mutex.set(:test_lock, Time.now - 60)
60
+ RedisMutex.on(:test_lock).set(Time.now - 60)
71
61
 
72
- mutex = Redis::Mutex.new(:test_lock)
73
- mutex.lock.should be_true
62
+ mutex = RedisMutex.new(:test_lock)
63
+ expect(mutex.lock).to be_truthy
74
64
  end
75
65
 
76
66
  it 'fails to unlock the key if it took too long past expiration' do
77
- mutex = Redis::Mutex.new(:test_lock, :expire => 0.1, :block => 0)
78
- mutex.lock.should be_true
67
+ mutex = RedisMutex.new(:test_lock, :expire => 0.1, :block => 0)
68
+ expect(mutex.lock).to be_truthy
79
69
  sleep 0.2 # lock expired
80
70
 
81
71
  # someone overwrites the expired lock
82
- mutex2 = Redis::Mutex.new(:test_lock, :expire => 10, :block => 0)
83
- mutex2.lock.should be_true
72
+ mutex2 = RedisMutex.new(:test_lock, :expire => 10, :block => 0)
73
+ expect(mutex2.lock).to be_truthy
84
74
 
85
75
  mutex.unlock
86
- mutex.get.should_not be_nil # lock should still be there
76
+ expect(mutex.get).not_to be_nil # lock should still be there
87
77
  end
88
78
 
89
79
  it 'ensures unlocking when something goes wrong in the block' do
90
- mutex = Redis::Mutex.new(:test_lock)
80
+ mutex = RedisMutex.new(:test_lock)
91
81
  begin
92
82
  mutex.with_lock do
93
83
  raise "Something went wrong!"
94
84
  end
95
85
  rescue RuntimeError
96
- mutex.get.should be_nil
86
+ expect(mutex.get).to be_nil
97
87
  end
98
88
  end
99
89
 
100
90
  it 'resets locking state on reuse' do
101
- mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
102
- mutex.lock.should be_true
103
- mutex.lock.should be_false
91
+ mutex = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
92
+ expect(mutex.lock).to be_truthy
93
+ expect(mutex.lock).to be_falsey
104
94
  end
105
95
 
106
96
  it 'tells about lock\'s state' do
107
- mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
97
+ mutex = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
108
98
  mutex.lock
109
99
 
110
- mutex.should be_locked
100
+ expect(mutex).to be_locked
111
101
 
112
102
  mutex.unlock
113
- mutex.should_not be_locked
103
+ expect(mutex).not_to be_locked
114
104
  end
115
105
 
116
106
  it 'tells that resource is not locked when lock is expired' do
117
- mutex = Redis::Mutex.new(:test_lock, :expire => 0.1)
107
+ mutex = RedisMutex.new(:test_lock, :expire => 0.1)
118
108
  mutex.lock
119
109
 
120
110
  sleep 0.2 # lock expired now
121
111
 
122
- mutex.should_not be_locked
112
+ expect(mutex).not_to be_locked
123
113
  end
124
114
 
125
115
  it 'returns value of block' do
126
- Redis::Mutex.with_lock(:test_lock) { :test_result }.should == :test_result
116
+ expect(RedisMutex.with_lock(:test_lock) { :test_result }).to eq(:test_result)
127
117
  end
128
118
 
129
119
  it 'requires block for #with_lock' do
130
- expect { Redis::Mutex.with_lock(:test_lock) }.to raise_error(LocalJumpError) #=> no block given (yield)
120
+ expect { RedisMutex.with_lock(:test_lock) }.to raise_error(LocalJumpError) #=> no block given (yield)
131
121
  end
132
122
 
133
123
  it 'raises LockError if lock not obtained' do
134
- expect { Redis::Mutex.lock!(:test_lock, SHORT_MUTEX_OPTIONS) }.to_not raise_error
135
- expect { Redis::Mutex.lock!(:test_lock, SHORT_MUTEX_OPTIONS) }.to raise_error(Redis::Mutex::LockError)
124
+ expect { RedisMutex.lock!(:test_lock, SHORT_MUTEX_OPTIONS) }.to_not raise_error
125
+ expect { RedisMutex.lock!(:test_lock, SHORT_MUTEX_OPTIONS) }.to raise_error(RedisMutex::LockError)
136
126
  end
137
127
 
138
128
  it 'raises UnlockError if lock not obtained' do
139
- mutex = Redis::Mutex.new(:test_lock)
140
- mutex.lock.should be_true
141
- mutex.unlock.should be_true
142
- expect { mutex.unlock! }.to raise_error(Redis::Mutex::UnlockError)
129
+ mutex = RedisMutex.new(:test_lock)
130
+ expect(mutex.lock).to be_truthy
131
+ expect(mutex.unlock).to be_truthy
132
+ expect { mutex.unlock! }.to raise_error(RedisMutex::UnlockError)
143
133
  end
144
134
 
145
135
  it 'raises AssertionError when block is given to #lock' do
146
136
  # instance method
147
- mutex = Redis::Mutex.new(:test_lock)
148
- expect { mutex.lock {} }.to raise_error(Redis::Mutex::AssertionError)
137
+ mutex = RedisMutex.new(:test_lock)
138
+ expect { mutex.lock {} }.to raise_error(RedisMutex::AssertionError)
149
139
 
150
140
  # class method
151
- expect { Redis::Mutex.lock(:test_lock) {} }.to raise_error(Redis::Mutex::AssertionError)
141
+ expect { RedisMutex.lock(:test_lock) {} }.to raise_error(RedisMutex::AssertionError)
152
142
  end
153
143
 
154
144
  it 'sweeps expired locks' do
155
- Redis::Mutex.set(:past, Time.now.to_f - 60)
156
- Redis::Mutex.set(:present, Time.now.to_f)
157
- Redis::Mutex.set(:future, Time.now.to_f + 60)
158
- Redis::Mutex.keys.size.should == 3
159
- Redis::Mutex.sweep.should == 2
160
- Redis::Mutex.keys.size.should == 1
161
- end
162
-
163
- describe Redis::Mutex::Macro do
164
- it 'adds auto_mutex' do
165
- t1 = Thread.new { C.new.run_singularly(1).should == "success: 1" }
166
- # In most cases t1 wins, but make sure to give it a head start,
167
- # not exceeding the sleep inside the method.
168
- sleep 0.01
169
- t2 = Thread.new { C.new.run_singularly(2).should == "failure: 2" }
170
- t1.join
171
- t2.join
145
+ RedisMutex.on(:past).set(Time.now.to_f - 60)
146
+ RedisMutex.on(:present).set(Time.now.to_f)
147
+ RedisMutex.on(:future).set(Time.now.to_f + 60)
148
+ expect(RedisMutex.keys.size).to eq(3)
149
+ expect(RedisMutex.sweep).to eq(2)
150
+ expect(RedisMutex.keys.size).to eq(1)
151
+ end
152
+
153
+ describe 'stress test' do
154
+ LOOP_NUM = 1000
155
+
156
+ def run(id)
157
+ print "invoked worker #{id}...\n"
158
+ mutex = RedisMutex.new(:test_lock, :expire => 1, :block => 10, :sleep => 0.01)
159
+ result = 0
160
+ LOOP_NUM.times do |i|
161
+ mutex.with_lock do
162
+ result += 1
163
+ sleep rand/100
164
+ end
165
+ end
166
+ print "result for worker #{id}: #{result} successful locks\n"
167
+ exit!(result == LOOP_NUM)
168
+ end
169
+
170
+ it 'runs without hiccups' do
171
+ begin
172
+ STDOUT.sync = true
173
+ puts "\nrunning stress tests..."
174
+ if pid1 = fork
175
+ # Parent
176
+ if pid2 = fork
177
+ # Parent
178
+ Process.waitall
179
+ else
180
+ # Child 2
181
+ run(2)
182
+ end
183
+ else
184
+ # Child 1
185
+ run(1)
186
+ end
187
+ STDOUT.flush
188
+ rescue NotImplementedError
189
+ puts $!
190
+ end
172
191
  end
173
192
  end
174
193
  end
@@ -6,9 +6,8 @@ require 'redis-mutex'
6
6
 
7
7
  RSpec.configure do |config|
8
8
  # Use database 15 for testing so we don't accidentally step on you real data.
9
- Redis::Classy.db = Redis.new(:db => 15)
10
- unless Redis::Classy.keys.empty?
11
- puts '[ERROR]: Redis database 15 not empty! If you are sure, run "rake flushdb" beforehand.'
12
- exit!
9
+ RedisClassy.redis = Redis.new(db: 15)
10
+ unless RedisClassy.keys.empty?
11
+ abort '[ERROR]: Redis database 15 not empty! If you are sure, run "rake flushdb" beforehand.'
13
12
  end
14
13
  end
metadata CHANGED
@@ -1,80 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-mutex
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 2.1.0
4
+ version: 4.0.2
6
5
  platform: ruby
7
6
  authors:
8
7
  - Kenn Ejima
9
- autorequire:
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-11 00:00:00.000000000 Z
11
+ date: 2020-07-04 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- version_requirements: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ~>
18
- - !ruby/object:Gem::Version
19
- version: '1.2'
20
- none: false
21
- prerelease: false
22
14
  name: redis-classy
23
15
  requirement: !ruby/object:Gem::Requirement
24
16
  requirements:
25
- - - ~>
17
+ - - "~>"
26
18
  - !ruby/object:Gem::Version
27
- version: '1.2'
28
- none: false
19
+ version: '2.0'
29
20
  type: :runtime
30
- - !ruby/object:Gem::Dependency
21
+ prerelease: false
31
22
  version_requirements: !ruby/object:Gem::Requirement
32
23
  requirements:
33
- - - ! '>='
24
+ - - "~>"
34
25
  - !ruby/object:Gem::Version
35
- version: '0'
36
- none: false
37
- prerelease: false
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
38
28
  name: rspec
39
29
  requirement: !ruby/object:Gem::Requirement
40
30
  requirements:
41
- - - ! '>='
31
+ - - ">="
42
32
  - !ruby/object:Gem::Version
43
33
  version: '0'
44
- none: false
45
34
  type: :development
46
- - !ruby/object:Gem::Dependency
35
+ prerelease: false
47
36
  version_requirements: !ruby/object:Gem::Requirement
48
37
  requirements:
49
- - - ! '>='
38
+ - - ">="
50
39
  - !ruby/object:Gem::Version
51
40
  version: '0'
52
- none: false
53
- prerelease: false
41
+ - !ruby/object:Gem::Dependency
54
42
  name: bundler
55
43
  requirement: !ruby/object:Gem::Requirement
56
44
  requirements:
57
- - - ! '>='
45
+ - - ">="
58
46
  - !ruby/object:Gem::Version
59
47
  version: '0'
60
- none: false
61
48
  type: :development
62
- - !ruby/object:Gem::Dependency
49
+ prerelease: false
63
50
  version_requirements: !ruby/object:Gem::Requirement
64
51
  requirements:
65
- - - ! '>='
52
+ - - ">="
66
53
  - !ruby/object:Gem::Version
67
54
  version: '0'
68
- none: false
69
- prerelease: false
55
+ - !ruby/object:Gem::Dependency
70
56
  name: rake
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
- - - ! '>='
59
+ - - ">="
74
60
  - !ruby/object:Gem::Version
75
61
  version: '0'
76
- none: false
77
62
  type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
78
69
  description: Distrubuted mutex using Redis
79
70
  email:
80
71
  - kenn.ejima@gmail.com
@@ -82,49 +73,43 @@ executables: []
82
73
  extensions: []
83
74
  extra_rdoc_files: []
84
75
  files:
85
- - .gitignore
86
- - .rspec
87
- - .travis.yml
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".travis.yml"
88
79
  - Gemfile
89
80
  - LICENSE
90
81
  - README.md
91
82
  - Rakefile
92
83
  - lib/redis-mutex.rb
93
- - lib/redis/mutex.rb
94
- - lib/redis/mutex/macro.rb
84
+ - lib/redis_mutex.rb
85
+ - lib/redis_mutex/macro.rb
95
86
  - redis-mutex.gemspec
87
+ - spec/redis_mutex_macro_spec.rb
96
88
  - spec/redis_mutex_spec.rb
97
89
  - spec/spec_helper.rb
98
90
  homepage: http://github.com/kenn/redis-mutex
99
91
  licenses: []
100
- post_install_message:
92
+ metadata: {}
93
+ post_install_message:
101
94
  rdoc_options: []
102
95
  require_paths:
103
96
  - lib
104
97
  required_ruby_version: !ruby/object:Gem::Requirement
105
98
  requirements:
106
- - - ! '>='
99
+ - - ">="
107
100
  - !ruby/object:Gem::Version
108
101
  version: '0'
109
- segments:
110
- - 0
111
- hash: -4158529389064232866
112
- none: false
113
102
  required_rubygems_version: !ruby/object:Gem::Requirement
114
103
  requirements:
115
- - - ! '>='
104
+ - - ">="
116
105
  - !ruby/object:Gem::Version
117
106
  version: '0'
118
- segments:
119
- - 0
120
- hash: -4158529389064232866
121
- none: false
122
107
  requirements: []
123
- rubyforge_project:
124
- rubygems_version: 1.8.24
125
- signing_key:
126
- specification_version: 3
108
+ rubygems_version: 3.0.8
109
+ signing_key:
110
+ specification_version: 4
127
111
  summary: Distrubuted mutex using Redis
128
112
  test_files:
113
+ - spec/redis_mutex_macro_spec.rb
129
114
  - spec/redis_mutex_spec.rb
130
115
  - spec/spec_helper.rb
@@ -1,130 +0,0 @@
1
- class Redis
2
- #
3
- # Options
4
- #
5
- # :block => Specify in seconds how long you want to wait for the lock to be released. Speficy 0
6
- # if you need non-blocking sematics and return false immediately. (default: 1)
7
- # :sleep => Specify in seconds how long the polling interval should be when :block is given.
8
- # It is recommended that you do NOT go below 0.01. (default: 0.1)
9
- # :expire => Specify in seconds when the lock should forcibly be removed when something went wrong
10
- # with the one who held the lock. (default: 10)
11
- #
12
- class Mutex < Redis::Classy
13
- autoload :Macro, 'redis/mutex/macro'
14
-
15
- DEFAULT_EXPIRE = 10
16
- LockError = Class.new(StandardError)
17
- UnlockError = Class.new(StandardError)
18
- AssertionError = Class.new(StandardError)
19
-
20
- def initialize(object, options={})
21
- super(object.is_a?(String) || object.is_a?(Symbol) ? object : "#{object.class.name}:#{object.id}")
22
- @block = options[:block] || 1
23
- @sleep = options[:sleep] || 0.1
24
- @expire = options[:expire] || DEFAULT_EXPIRE
25
- end
26
-
27
- def lock
28
- self.class.raise_assertion_error if block_given?
29
- @locking = false
30
-
31
- if @block > 0
32
- # Blocking mode
33
- start_at = Time.now
34
- while Time.now - start_at < @block
35
- @locking = true and break if try_lock
36
- sleep @sleep
37
- end
38
- else
39
- # Non-blocking mode
40
- @locking = try_lock
41
- end
42
- @locking
43
- end
44
-
45
- def try_lock
46
- now = Time.now.to_f
47
- @expires_at = now + @expire # Extend in each blocking loop
48
- return true if setnx(@expires_at) # Success, the lock has been acquired
49
- return false if get.to_f > now # Check if the lock is still effective
50
-
51
- # The lock has expired but wasn't released... BAD!
52
- return true if getset(@expires_at).to_f <= now # Success, we acquired the previously expired lock
53
- return false # Dammit, it seems that someone else was even faster than us to remove the expired lock!
54
- end
55
-
56
- # Returns true if resource is locked. Note that nil.to_f returns 0.0
57
- def locked?
58
- get.to_f > Time.now.to_f
59
- end
60
-
61
- def unlock(force = false)
62
- # Since it's possible that the operations in the critical section took a long time,
63
- # we can't just simply release the lock. The unlock method checks if @expires_at
64
- # remains the same, and do not release when the lock timestamp was overwritten.
65
-
66
- if get == @expires_at.to_s or force
67
- # Redis#del with a single key returns '1' or nil
68
- !!del
69
- else
70
- false
71
- end
72
- end
73
-
74
- def with_lock
75
- if lock!
76
- begin
77
- @result = yield
78
- ensure
79
- unlock
80
- end
81
- end
82
- @result
83
- end
84
-
85
- def lock!
86
- lock or raise LockError, "failed to acquire lock #{key.inspect}"
87
- end
88
-
89
- def unlock!(force = false)
90
- unlock(force) or raise UnlockError, "failed to release lock #{key.inspect}"
91
- end
92
-
93
- class << self
94
- def sweep
95
- return 0 if (all_keys = keys).empty?
96
-
97
- now = Time.now.to_f
98
- values = mget(*all_keys)
99
-
100
- expired_keys = all_keys.zip(values).select do |key, time|
101
- time && time.to_f <= now
102
- end
103
-
104
- expired_keys.each do |key, _|
105
- # Make extra sure that anyone haven't extended the lock
106
- del(key) if getset(key, now + DEFAULT_EXPIRE).to_f <= now
107
- end
108
-
109
- expired_keys.size
110
- end
111
-
112
- def lock(object, options = {})
113
- raise_assertion_error if block_given?
114
- new(object, options).lock
115
- end
116
-
117
- def lock!(object, options = {})
118
- new(object, options).lock!
119
- end
120
-
121
- def with_lock(object, options = {}, &block)
122
- new(object, options).with_lock(&block)
123
- end
124
-
125
- def raise_assertion_error
126
- raise AssertionError, 'block syntax has been removed from #lock, use #with_lock instead'
127
- end
128
- end
129
- end
130
- end
@@ -1,50 +0,0 @@
1
- class Redis
2
- class Mutex
3
- module Macro
4
- def self.included(base)
5
- base.extend ClassMethods
6
- base.class_eval do
7
- class << self
8
- attr_accessor :auto_mutex_methods
9
- end
10
- @auto_mutex_methods = {}
11
- end
12
- end
13
-
14
- module ClassMethods
15
- def auto_mutex(target, options={})
16
- self.auto_mutex_methods[target] = options
17
- end
18
-
19
- def method_added(target)
20
- return if target.to_s =~ /^auto_mutex_methods/
21
- return unless self.auto_mutex_methods[target]
22
- without_method = "#{target}_without_auto_mutex"
23
- with_method = "#{target}_with_auto_mutex"
24
- after_method = "#{target}_after_failure"
25
- return if method_defined?(without_method)
26
- options = self.auto_mutex_methods[target]
27
-
28
- if options[:after_failure].is_a?(Proc)
29
- define_method(after_method, &options[:after_failure])
30
- end
31
-
32
- define_method(with_method) do |*args|
33
- key = self.class.name << '#' << target.to_s
34
-
35
- begin
36
- Redis::Mutex.with_lock(key, options) do
37
- send(without_method, *args)
38
- end
39
- rescue Redis::Mutex::LockError
40
- send(after_method, *args) if respond_to?(after_method)
41
- end
42
- end
43
-
44
- alias_method without_method, target
45
- alias_method target, with_method
46
- end
47
- end
48
- end
49
- end
50
- end