redis-mutex 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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