redis-mutex 3.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 31cd09963698ff45f5481699862332a7299ea0fb
4
- data.tar.gz: ca58764a3e815285dc03a03650969f20db9c65fc
3
+ metadata.gz: 253615b378afcd470a670355dc6924fc18ce9bad
4
+ data.tar.gz: 22fdcf12b682dd3550a6a73b2ef1b0aa96aaa5b3
5
5
  SHA512:
6
- metadata.gz: d6cc1d509749ac3214d6bdfe8d02e1c91dcde89fc901e54c6019a451aba47c7592581eee63cf7dd0eea9a00f0776acca86d64850712929f6f29de9dadef95a6c
7
- data.tar.gz: fd155b4e46710e57b0e95425ae896abdcb7a2efa7e0ddd872915156ffd2e92a0ef0933b433b0bfab0b8f399f46e500c7ee329bdb4c002a294dcef5d438a404b7
6
+ metadata.gz: e4e348e87e282e279e3bb4a780f420484365ceaa101438de710f7b0fe5ed9843278d3615579c343497d23041980614fbd2772c2a85f86ec4029800e92104b888
7
+ data.tar.gz: 06897c56818022fae3b8b97a8a391f1d72e06887916fb08ed81d58f3b37341063a4c15feff782f82ed6a7072a342a66d501a7e44e4e213f6774c489a8b83602e
@@ -4,6 +4,6 @@ services:
4
4
  rvm:
5
5
  - 2.0.0
6
6
  - 2.1.0
7
- - rbx
7
+ - rbx-2
8
8
  - ruby-head
9
9
  - jruby-head
data/README.md CHANGED
@@ -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
@@ -40,13 +40,21 @@ Or if you want to immediately receive `false` on an unsuccessful locking attempt
40
40
  Changelog
41
41
  ---------
42
42
 
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
+
43
50
  ### v3.0
44
51
 
45
52
  * Ruby 2.0 or later is required.
53
+ * `auto_mutex` now takes `:on` for additional key scoping.
46
54
 
47
55
  ### v2.0
48
56
 
49
- * **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.
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.
50
58
  * **INCOMPATIBLE CHANGE**: `#lock` no longer accepts a block. Use `#with_lock` instead, which uses `lock!` internally and returns the value of block.
51
59
  * `unlock` returns boolean values for success / failure, for consistency with `lock`.
52
60
 
@@ -67,7 +75,7 @@ gem 'redis-mutex'
67
75
  Register the Redis server: (e.g. in `config/initializers/redis_mutex.rb` for Rails)
68
76
 
69
77
  ```ruby
70
- Redis::Classy.db = Redis.new(:host => 'localhost')
78
+ RedisClassy.redis = Redis.new
71
79
  ```
72
80
 
73
81
  Note that Redis Mutex uses the `redis-classy` gem internally to organize keys in an isolated namespace.
@@ -75,7 +83,7 @@ Note that Redis Mutex uses the `redis-classy` gem internally to organize keys in
75
83
  There are a number of methods:
76
84
 
77
85
  ```ruby
78
- mutex = Redis::Mutex.new(key, options) # Configure a mutex lock
86
+ mutex = RedisMutex.new(key, options) # Configure a mutex lock
79
87
  mutex.lock # Try to acquire the lock, returns false when failed
80
88
  mutex.lock! # Try to acquire the lock, raises exception when failed
81
89
  mutex.unlock # Try to release the lock, returns false when failed
@@ -84,20 +92,20 @@ mutex.locked? # Find out if resource already locked
84
92
  mutex.with_lock # Try to acquire the lock, execute the block, then return the value of the block.
85
93
  # Raises exception when failed to acquire the lock.
86
94
 
87
- Redis::Mutex.sweep # Remove all expired locks
88
- 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
89
97
  ```
90
98
 
91
99
  The key argument can be symbol, string, or any Ruby objects that respond to `id` method, where the key is automatically set as
92
- `TheClass:id`. For any given key, `Redis::Mutex:` prefix will be automatically prepended. For instance, if you pass a `Room`
93
- object with id of `123`, the actual key in Redis will be `Redis::Mutex:Room:123`. The automatic prefixing and instance binding
94
- 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).
95
103
 
96
104
  The initialize method takes several options.
97
105
 
98
106
  ```ruby
99
107
  :block => 1 # Specify in seconds how long you want to wait for the lock to be released.
100
- # 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)
101
109
  :sleep => 0.1 # Specify in seconds how long the polling interval should be when :block is given.
102
110
  # It is NOT recommended to go below 0.01. (default: 0.1)
103
111
  :expire => 10 # Specify in seconds when the lock should be considered stale when something went wrong
@@ -114,11 +122,11 @@ class RoomController < ApplicationController
114
122
  before_filter { @room = Room.find(params[:id]) }
115
123
 
116
124
  def enter
117
- Redis::Mutex.with_lock(@room) do # key => "Room:123"
125
+ RedisMutex.with_lock(@room) do # key => "Room:123"
118
126
  # do something exclusively
119
127
  end
120
128
  render text: 'success!'
121
- rescue Redis::Mutex::LockError
129
+ rescue RedisMutex::LockError
122
130
  render text: 'failed to acquire lock!'
123
131
  end
124
132
  end
@@ -129,7 +137,7 @@ put the `unlock` method in the `ensure` clause.
129
137
 
130
138
  ```ruby
131
139
  def enter
132
- mutex = Redis::Mutex.new('non-blocking', block: 0, expire: 10.minutes)
140
+ mutex = RedisMutex.new('non-blocking', block: 0, expire: 10.minutes)
133
141
  if mutex.lock
134
142
  begin
135
143
  # do something exclusively
@@ -153,7 +161,7 @@ If you give a proc object to the `after_failure` option, it will get called afte
153
161
 
154
162
  ```ruby
155
163
  class JobController < ApplicationController
156
- include Redis::Mutex::Macro
164
+ include RedisMutex::Macro
157
165
  auto_mutex :run, block: 0, after_failure: lambda { render text: 'failed to acquire lock!' }
158
166
 
159
167
  def run
@@ -162,3 +170,17 @@ class JobController < ApplicationController
162
170
  end
163
171
  end
164
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
+ loop do
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,56 @@
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 = self.class.name << '#' << target.to_s << ":" << arguments.join(':')
42
+ begin
43
+ RedisMutex.with_lock(key, options) do
44
+ send(without_method, *args)
45
+ end
46
+ rescue RedisMutex::LockError
47
+ send(after_method, *args) if respond_to?(after_method)
48
+ end
49
+ end
50
+
51
+ alias_method without_method, target
52
+ alias_method target, with_method
53
+ end
54
+ end
55
+ end
56
+ 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 = '3.0.0' # retrieve this value by: Gem.loaded_specs['redis-mutex'].version.to_s
15
+ gem.version = '4.0.0' # 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
 
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  class C
4
- include Redis::Mutex::Macro
4
+ include RedisMutex::Macro
5
5
  auto_mutex :run_singularly, :block => 0, :after_failure => lambda {|id| return "failure: #{id}" }
6
6
 
7
7
  def run_singularly(id)
@@ -22,7 +22,7 @@ class C
22
22
  end
23
23
  end
24
24
 
25
- describe Redis::Mutex::Macro do
25
+ describe RedisMutex::Macro do
26
26
 
27
27
  def race(a, b)
28
28
  t1 = Thread.new(&a)
@@ -38,32 +38,32 @@ describe Redis::Mutex::Macro do
38
38
 
39
39
  it 'adds auto_mutex' do
40
40
  race(
41
- proc { C.new.run_singularly(1).should == "success: 1" },
42
- proc { C.new.run_singularly(2).should == "failure: 2" })
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
43
  end
44
44
 
45
45
  it 'adds auto_mutex on different args' do
46
46
  race(
47
- proc { C.new.run_singularly_on_args(1, :'2', object_arg).should == "success: 1" },
48
- proc { C.new.run_singularly_on_args(2, :'2', object_arg).should == "success: 2" })
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
49
  end
50
50
 
51
51
  it 'adds auto_mutex on same args' do
52
52
  race(
53
- proc { C.new.run_singularly_on_args(1, :'2', object_arg).should == "success: 1" },
54
- proc { C.new.run_singularly_on_args(1, :'2', object_arg).should == "failure: 1" })
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
55
  end
56
56
 
57
57
  it 'adds auto_mutex on different keyword args' do
58
58
  race(
59
- proc { C.new.run_singularly_on_keyword_args(id: 1, foo: :'2', bar: object_arg).should == "success: 1" },
60
- proc { C.new.run_singularly_on_keyword_args(id: 2, foo: :'2', bar: object_arg).should == "success: 2" })
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
61
  end
62
62
 
63
63
  it 'adds auto_mutex on same keyword args' do
64
64
  race(
65
- proc { C.new.run_singularly_on_keyword_args(id: 1, foo: :'2', bar: object_arg).should == "success: 1" },
66
- proc { C.new.run_singularly_on_keyword_args(id: 1, foo: :'2', bar: object_arg).should == "failure: 1" })
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
67
  end
68
68
 
69
69
  it 'raise exception if there is no such argument' do
@@ -2,152 +2,152 @@ require 'spec_helper'
2
2
 
3
3
  SHORT_MUTEX_OPTIONS = { :block => 0.1, :sleep => 0.02 }
4
4
 
5
- describe Redis::Mutex do
5
+ describe RedisMutex do
6
6
  before do
7
- Redis::Classy.flushdb
7
+ RedisClassy.flushdb
8
8
  end
9
9
 
10
10
  after :all do
11
- Redis::Classy.flushdb
12
- Redis::Classy.quit
11
+ RedisClassy.flushdb
12
+ RedisClassy.quit
13
13
  end
14
14
 
15
15
  it 'locks the universe' do
16
- mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
17
- mutex1.lock.should be_true
16
+ mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
17
+ expect(mutex1.lock).to be_truthy
18
18
 
19
- mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
20
- mutex2.lock.should be_false
19
+ mutex2 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
20
+ expect(mutex2.lock).to be_falsey
21
21
  end
22
22
 
23
23
  it 'fails to lock when the lock is taken' do
24
- mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
24
+ mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
25
25
 
26
- mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
27
- 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
28
28
 
29
- mutex1.lock.should be_false # fail
29
+ expect(mutex1.lock).to be_falsey # fail
30
30
  end
31
31
 
32
32
  it 'unlocks only once' do
33
- mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
34
- mutex.lock.should be_true
33
+ mutex = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
34
+ expect(mutex.lock).to be_truthy
35
35
 
36
- mutex.unlock.should be_true # successfully released the lock
37
- 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
38
38
  end
39
39
 
40
40
  it 'prevents accidental unlock from outside' do
41
- mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
42
- mutex1.lock.should be_true
41
+ mutex1 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
42
+ expect(mutex1.lock).to be_truthy
43
43
 
44
- mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
45
- mutex2.unlock.should be_false
44
+ mutex2 = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
45
+ expect(mutex2.unlock).to be_falsey
46
46
  end
47
47
 
48
48
  it 'sets expiration' do
49
49
  start = Time.now
50
50
  expires_in = 10
51
- mutex = Redis::Mutex.new(:test_lock, :expire => expires_in)
51
+ mutex = RedisMutex.new(:test_lock, :expire => expires_in)
52
52
  mutex.with_lock do
53
- 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)
54
54
  end
55
- mutex.get.should be_nil # key should have been cleaned up
55
+ expect(mutex.get).to be_nil # key should have been cleaned up
56
56
  end
57
57
 
58
58
  it 'overwrites a lock when existing lock is expired' do
59
59
  # stale lock from the far past
60
- Redis::Mutex.set(:test_lock, Time.now - 60)
60
+ RedisMutex.on(:test_lock).set(Time.now - 60)
61
61
 
62
- mutex = Redis::Mutex.new(:test_lock)
63
- mutex.lock.should be_true
62
+ mutex = RedisMutex.new(:test_lock)
63
+ expect(mutex.lock).to be_truthy
64
64
  end
65
65
 
66
66
  it 'fails to unlock the key if it took too long past expiration' do
67
- mutex = Redis::Mutex.new(:test_lock, :expire => 0.1, :block => 0)
68
- mutex.lock.should be_true
67
+ mutex = RedisMutex.new(:test_lock, :expire => 0.1, :block => 0)
68
+ expect(mutex.lock).to be_truthy
69
69
  sleep 0.2 # lock expired
70
70
 
71
71
  # someone overwrites the expired lock
72
- mutex2 = Redis::Mutex.new(:test_lock, :expire => 10, :block => 0)
73
- mutex2.lock.should be_true
72
+ mutex2 = RedisMutex.new(:test_lock, :expire => 10, :block => 0)
73
+ expect(mutex2.lock).to be_truthy
74
74
 
75
75
  mutex.unlock
76
- mutex.get.should_not be_nil # lock should still be there
76
+ expect(mutex.get).not_to be_nil # lock should still be there
77
77
  end
78
78
 
79
79
  it 'ensures unlocking when something goes wrong in the block' do
80
- mutex = Redis::Mutex.new(:test_lock)
80
+ mutex = RedisMutex.new(:test_lock)
81
81
  begin
82
82
  mutex.with_lock do
83
83
  raise "Something went wrong!"
84
84
  end
85
85
  rescue RuntimeError
86
- mutex.get.should be_nil
86
+ expect(mutex.get).to be_nil
87
87
  end
88
88
  end
89
89
 
90
90
  it 'resets locking state on reuse' do
91
- mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
92
- mutex.lock.should be_true
93
- 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
94
94
  end
95
95
 
96
96
  it 'tells about lock\'s state' do
97
- mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
97
+ mutex = RedisMutex.new(:test_lock, SHORT_MUTEX_OPTIONS)
98
98
  mutex.lock
99
99
 
100
- mutex.should be_locked
100
+ expect(mutex).to be_locked
101
101
 
102
102
  mutex.unlock
103
- mutex.should_not be_locked
103
+ expect(mutex).not_to be_locked
104
104
  end
105
105
 
106
106
  it 'tells that resource is not locked when lock is expired' do
107
- mutex = Redis::Mutex.new(:test_lock, :expire => 0.1)
107
+ mutex = RedisMutex.new(:test_lock, :expire => 0.1)
108
108
  mutex.lock
109
109
 
110
110
  sleep 0.2 # lock expired now
111
111
 
112
- mutex.should_not be_locked
112
+ expect(mutex).not_to be_locked
113
113
  end
114
114
 
115
115
  it 'returns value of block' do
116
- Redis::Mutex.with_lock(:test_lock) { :test_result }.should == :test_result
116
+ expect(RedisMutex.with_lock(:test_lock) { :test_result }).to eq(:test_result)
117
117
  end
118
118
 
119
119
  it 'requires block for #with_lock' do
120
- 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)
121
121
  end
122
122
 
123
123
  it 'raises LockError if lock not obtained' do
124
- expect { Redis::Mutex.lock!(:test_lock, SHORT_MUTEX_OPTIONS) }.to_not raise_error
125
- 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)
126
126
  end
127
127
 
128
128
  it 'raises UnlockError if lock not obtained' do
129
- mutex = Redis::Mutex.new(:test_lock)
130
- mutex.lock.should be_true
131
- mutex.unlock.should be_true
132
- 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)
133
133
  end
134
134
 
135
135
  it 'raises AssertionError when block is given to #lock' do
136
136
  # instance method
137
- mutex = Redis::Mutex.new(:test_lock)
138
- expect { mutex.lock {} }.to raise_error(Redis::Mutex::AssertionError)
137
+ mutex = RedisMutex.new(:test_lock)
138
+ expect { mutex.lock {} }.to raise_error(RedisMutex::AssertionError)
139
139
 
140
140
  # class method
141
- expect { Redis::Mutex.lock(:test_lock) {} }.to raise_error(Redis::Mutex::AssertionError)
141
+ expect { RedisMutex.lock(:test_lock) {} }.to raise_error(RedisMutex::AssertionError)
142
142
  end
143
143
 
144
144
  it 'sweeps expired locks' do
145
- Redis::Mutex.set(:past, Time.now.to_f - 60)
146
- Redis::Mutex.set(:present, Time.now.to_f)
147
- Redis::Mutex.set(:future, Time.now.to_f + 60)
148
- Redis::Mutex.keys.size.should == 3
149
- Redis::Mutex.sweep.should == 2
150
- Redis::Mutex.keys.size.should == 1
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
151
  end
152
152
 
153
153
  describe 'stress test' do
@@ -155,8 +155,8 @@ describe Redis::Mutex do
155
155
 
156
156
  def run(id)
157
157
  print "invoked worker #{id}...\n"
158
- Redis::Classy.db.client.reconnect
159
- mutex = Redis::Mutex.new(:test_lock, :expire => 1, :block => 10, :sleep => 0.01)
158
+ RedisClassy.redis.client.reconnect
159
+ mutex = RedisMutex.new(:test_lock, :expire => 1, :block => 10, :sleep => 0.01)
160
160
  result = 0
161
161
  LOOP_NUM.times do |i|
162
162
  mutex.with_lock do
@@ -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,69 +1,69 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-mutex
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenn Ejima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-28 00:00:00.000000000 Z
11
+ date: 2014-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-classy
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.2'
19
+ version: '2.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.2'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  description: Distrubuted mutex using Redis
@@ -73,16 +73,16 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
- - .gitignore
77
- - .rspec
78
- - .travis.yml
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".travis.yml"
79
79
  - Gemfile
80
80
  - LICENSE
81
81
  - README.md
82
82
  - Rakefile
83
83
  - lib/redis-mutex.rb
84
- - lib/redis/mutex.rb
85
- - lib/redis/mutex/macro.rb
84
+ - lib/redis_mutex.rb
85
+ - lib/redis_mutex/macro.rb
86
86
  - redis-mutex.gemspec
87
87
  - spec/redis_mutex_macro_spec.rb
88
88
  - spec/redis_mutex_spec.rb
@@ -96,17 +96,17 @@ require_paths:
96
96
  - lib
97
97
  required_ruby_version: !ruby/object:Gem::Requirement
98
98
  requirements:
99
- - - '>='
99
+ - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
102
  required_rubygems_version: !ruby/object:Gem::Requirement
103
103
  requirements:
104
- - - '>='
104
+ - - ">="
105
105
  - !ruby/object:Gem::Version
106
106
  version: '0'
107
107
  requirements: []
108
108
  rubyforge_project:
109
- rubygems_version: 2.0.14
109
+ rubygems_version: 2.2.2
110
110
  signing_key:
111
111
  specification_version: 4
112
112
  summary: Distrubuted mutex using Redis
@@ -1,134 +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
-
49
- loop do
50
- return true if setnx(@expires_at) # Success, the lock has been acquired
51
- end until old_value = get # Repeat if unlocked before get
52
-
53
- return false if old_value.to_f > now # Check if the lock is still effective
54
-
55
- # The lock has expired but wasn't released... BAD!
56
- return true if getset(@expires_at).to_f <= now # Success, we acquired the previously expired lock
57
- return false # Dammit, it seems that someone else was even faster than us to remove the expired lock!
58
- end
59
-
60
- # Returns true if resource is locked. Note that nil.to_f returns 0.0
61
- def locked?
62
- get.to_f > Time.now.to_f
63
- end
64
-
65
- def unlock(force = false)
66
- # Since it's possible that the operations in the critical section took a long time,
67
- # we can't just simply release the lock. The unlock method checks if @expires_at
68
- # remains the same, and do not release when the lock timestamp was overwritten.
69
-
70
- if get == @expires_at.to_s or force
71
- # Redis#del with a single key returns '1' or nil
72
- !!del
73
- else
74
- false
75
- end
76
- end
77
-
78
- def with_lock
79
- if lock!
80
- begin
81
- @result = yield
82
- ensure
83
- unlock
84
- end
85
- end
86
- @result
87
- end
88
-
89
- def lock!
90
- lock or raise LockError, "failed to acquire lock #{key.inspect}"
91
- end
92
-
93
- def unlock!(force = false)
94
- unlock(force) or raise UnlockError, "failed to release lock #{key.inspect}"
95
- end
96
-
97
- class << self
98
- def sweep
99
- return 0 if (all_keys = keys).empty?
100
-
101
- now = Time.now.to_f
102
- values = mget(*all_keys)
103
-
104
- expired_keys = all_keys.zip(values).select do |key, time|
105
- time && time.to_f <= now
106
- end
107
-
108
- expired_keys.each do |key, _|
109
- # Make extra sure that anyone haven't extended the lock
110
- del(key) if getset(key, now + DEFAULT_EXPIRE).to_f <= now
111
- end
112
-
113
- expired_keys.size
114
- end
115
-
116
- def lock(object, options = {})
117
- raise_assertion_error if block_given?
118
- new(object, options).lock
119
- end
120
-
121
- def lock!(object, options = {})
122
- new(object, options).lock!
123
- end
124
-
125
- def with_lock(object, options = {}, &block)
126
- new(object, options).with_lock(&block)
127
- end
128
-
129
- def raise_assertion_error
130
- raise AssertionError, 'block syntax has been removed from #lock, use #with_lock instead'
131
- end
132
- end
133
- end
134
- end
@@ -1,58 +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
- target_argument_names = instance_method(target.to_sym).parameters.map(&:last)
32
- on_arguments = Array(options[:on])
33
- mutex_arguments = on_arguments & target_argument_names
34
- unknown_arguments = on_arguments - target_argument_names
35
- if unknown_arguments.any?
36
- raise ArgumentError, "You are trying to lock on unknown arguments: #{unknown_arguments.join(', ')}"
37
- end
38
-
39
- define_method(with_method) do |*args|
40
- named_arguments = Hash[target_argument_names.zip(args)]
41
- arguments = mutex_arguments.map { |name| named_arguments.fetch(name) }
42
- key = self.class.name << '#' << target.to_s << ":" << arguments.join(':')
43
- begin
44
- Redis::Mutex.with_lock(key, options) do
45
- send(without_method, *args)
46
- end
47
- rescue Redis::Mutex::LockError
48
- send(after_method, *args) if respond_to?(after_method)
49
- end
50
- end
51
-
52
- alias_method without_method, target
53
- alias_method target, with_method
54
- end
55
- end
56
- end
57
- end
58
- end