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.
@@ -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