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.
- 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
|
[![Build Status](https://secure.travis-ci.org/kenn/redis-mutex.png)](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
|