redis-mutex 2.1.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.travis.yml +9 -9
- data/README.md +46 -18
- data/lib/redis-mutex.rb +1 -4
- data/lib/redis_mutex.rb +132 -0
- data/lib/redis_mutex/macro.rb +61 -0
- data/redis-mutex.gemspec +2 -2
- data/spec/redis_mutex_macro_spec.rb +81 -0
- data/spec/redis_mutex_spec.rb +98 -79
- data/spec/spec_helper.rb +3 -4
- metadata +38 -53
- data/lib/redis/mutex.rb +0 -130
- data/lib/redis/mutex/macro.rb +0 -50
checksums.yaml
ADDED
@@ -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
|
data/.travis.yml
CHANGED
@@ -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
|
4
|
-
-
|
5
|
-
-
|
6
|
-
-
|
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
|
[](http://travis-ci.org/kenn/redis-mutex)
|
5
5
|
|
6
|
-
|
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
|
-
|
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 =
|
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
|
-
|
41
|
-
|
40
|
+
Changelog
|
41
|
+
---------
|
42
42
|
|
43
|
-
|
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
|
-
|
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 =
|
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
|
-
|
82
|
-
|
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, `
|
87
|
-
object with id of `123`, the actual key in Redis will be `
|
88
|
-
is the feature of `
|
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
|
-
#
|
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
|
-
|
125
|
+
RedisMutex.with_lock(@room) do # key => "Room:123"
|
112
126
|
# do something exclusively
|
113
127
|
end
|
114
128
|
render text: 'success!'
|
115
|
-
rescue
|
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 =
|
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
|
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
|
+
```
|
data/lib/redis-mutex.rb
CHANGED
data/lib/redis_mutex.rb
ADDED
@@ -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
|
data/redis-mutex.gemspec
CHANGED
@@ -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 = '
|
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", "~>
|
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
|
data/spec/redis_mutex_spec.rb
CHANGED
@@ -2,173 +2,192 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
SHORT_MUTEX_OPTIONS = { :block => 0.1, :sleep => 0.02 }
|
4
4
|
|
5
|
-
|
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
|
-
|
7
|
+
RedisClassy.flushdb
|
18
8
|
end
|
19
9
|
|
20
10
|
after :all do
|
21
|
-
|
22
|
-
|
11
|
+
RedisClassy.flushdb
|
12
|
+
RedisClassy.quit
|
23
13
|
end
|
24
14
|
|
25
15
|
it 'locks the universe' do
|
26
|
-
mutex1 =
|
27
|
-
mutex1.lock.
|
16
|
+
mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
|
17
|
+
expect(mutex1.lock).to be_truthy
|
28
18
|
|
29
|
-
mutex2 =
|
30
|
-
mutex2.lock.
|
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 =
|
24
|
+
mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
|
35
25
|
|
36
|
-
mutex2 =
|
37
|
-
mutex2.lock.
|
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.
|
29
|
+
expect(mutex1.lock).to be_falsey # fail
|
40
30
|
end
|
41
31
|
|
42
32
|
it 'unlocks only once' do
|
43
|
-
mutex =
|
44
|
-
mutex.lock.
|
33
|
+
mutex = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
|
34
|
+
expect(mutex.lock).to be_truthy
|
45
35
|
|
46
|
-
mutex.unlock.
|
47
|
-
mutex.unlock.
|
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 =
|
52
|
-
mutex1.lock.
|
41
|
+
mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
|
42
|
+
expect(mutex1.lock).to be_truthy
|
53
43
|
|
54
|
-
mutex2 =
|
55
|
-
mutex2.unlock.
|
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 =
|
51
|
+
mutex = RedisMutex.new(:test_lock, :expire => expires_in)
|
62
52
|
mutex.with_lock do
|
63
|
-
mutex.get.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.
|
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
|
-
|
60
|
+
RedisMutex.on(:test_lock).set(Time.now - 60)
|
71
61
|
|
72
|
-
mutex =
|
73
|
-
mutex.lock.
|
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 =
|
78
|
-
mutex.lock.
|
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 =
|
83
|
-
mutex2.lock.
|
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.
|
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 =
|
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.
|
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 =
|
102
|
-
mutex.lock.
|
103
|
-
mutex.lock.
|
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 =
|
97
|
+
mutex = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
|
108
98
|
mutex.lock
|
109
99
|
|
110
|
-
mutex.
|
100
|
+
expect(mutex).to be_locked
|
111
101
|
|
112
102
|
mutex.unlock
|
113
|
-
mutex.
|
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 =
|
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.
|
112
|
+
expect(mutex).not_to be_locked
|
123
113
|
end
|
124
114
|
|
125
115
|
it 'returns value of block' do
|
126
|
-
|
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 {
|
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 {
|
135
|
-
expect {
|
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 =
|
140
|
-
mutex.lock.
|
141
|
-
mutex.unlock.
|
142
|
-
expect { mutex.unlock! }.to raise_error(
|
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 =
|
148
|
-
expect { mutex.lock {} }.to raise_error(
|
137
|
+
mutex = RedisMutex.new(:test_lock)
|
138
|
+
expect { mutex.lock {} }.to raise_error(RedisMutex::AssertionError)
|
149
139
|
|
150
140
|
# class method
|
151
|
-
expect {
|
141
|
+
expect { RedisMutex.lock(:test_lock) {} }.to raise_error(RedisMutex::AssertionError)
|
152
142
|
end
|
153
143
|
|
154
144
|
it 'sweeps expired locks' do
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
end
|
162
|
-
|
163
|
-
describe
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
sleep 0.01
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
-
|
10
|
-
unless
|
11
|
-
|
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
|
-
|
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:
|
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: '
|
28
|
-
none: false
|
19
|
+
version: '2.0'
|
29
20
|
type: :runtime
|
30
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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/
|
94
|
-
- lib/
|
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
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
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
|
data/lib/redis/mutex.rb
DELETED
@@ -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
|
data/lib/redis/mutex/macro.rb
DELETED
@@ -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
|