redis-mutex 0.9.0 → 1.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.
- data/README.rdoc +40 -16
- data/Rakefile +6 -3
- data/VERSION +1 -1
- data/lib/redis/mutex.rb +83 -0
- data/lib/redis-mutex.rb +2 -2
- data/redis-mutex.gemspec +65 -0
- data/spec/redis_mutex_spec.rb +53 -0
- data/spec/spec_helper.rb +7 -1
- metadata +7 -6
- data/lib/redis/classy/mutex.rb +0 -45
- data/spec/redis-mutex_spec.rb +0 -7
data/README.rdoc
CHANGED
@@ -1,11 +1,25 @@
|
|
1
1
|
= Redis Mutex
|
2
2
|
|
3
|
-
Distrubuted
|
3
|
+
Distrubuted mutex in Ruby using Redis. Supports both blocking and non-blocking semantics.
|
4
4
|
|
5
|
-
The idea was taken from
|
5
|
+
The idea was taken from http://redis.io/commands/setnx and http://gist.github.com/457269,
|
6
|
+
also block syntax was inspired by https://github.com/nateware/redis-objects.
|
6
7
|
|
7
8
|
Requires the redis-classy gem.
|
8
9
|
|
10
|
+
== Synopsis
|
11
|
+
|
12
|
+
Initialize a Redis::Mutex object with a key, then only one thread / process / server can
|
13
|
+
enter the locked block at a time.
|
14
|
+
|
15
|
+
mutex = Redis::Mutex.new(:lock_name)
|
16
|
+
mutex.lock do
|
17
|
+
do_something
|
18
|
+
end
|
19
|
+
|
20
|
+
By default, when one is running the locked block, others wait 1 second, polling interval is 100ms.
|
21
|
+
If 1 second has passed, the lock method returns false.
|
22
|
+
|
9
23
|
== Install
|
10
24
|
|
11
25
|
gem install redis-mutex
|
@@ -16,18 +30,31 @@ In Gemfile:
|
|
16
30
|
|
17
31
|
gem "redis-mutex"
|
18
32
|
|
19
|
-
|
33
|
+
Create a reference to Redis. (e.g. in config/initializers/redis_mutex.rb for Rails)
|
20
34
|
|
21
35
|
Redis::Classy.db = Redis.new(:host => 'localhost')
|
22
36
|
|
23
37
|
There are four methods - new, lock, unlock and sweep:
|
24
38
|
|
25
|
-
mutex = Redis::
|
39
|
+
mutex = Redis::Mutex.new(key, options)
|
26
40
|
mutex.lock
|
27
41
|
mutex.unlock
|
28
|
-
Redis::
|
42
|
+
Redis::Mutex.sweep
|
29
43
|
|
30
|
-
|
44
|
+
For the key, it takes any Ruby objects that respond to :id, where the key is automatically set as "TheClass:id",
|
45
|
+
or pass any string or symbol.
|
46
|
+
|
47
|
+
Also the initialize method takes several options.
|
48
|
+
|
49
|
+
:block => Specify in seconds how long you want to wait for the lock to be released. Speficy 0
|
50
|
+
if you need non-blocking sematics and return false immediately. (default: 1)
|
51
|
+
:sleep => Specify in seconds how long the polling interval should be when :block is given.
|
52
|
+
It is recommended that you do NOT go below 0.01. (default: 0.1)
|
53
|
+
:expire => Specify in seconds when the lock should forcibly be removed when something went wrong
|
54
|
+
with the one who held the lock. (in seconds, default: 10)
|
55
|
+
|
56
|
+
The lock method returns true when the lock has been successfully obtained, or returns false when the attempts
|
57
|
+
failed after the seconds specified with :block. It immediately returns false when 0 is given to :block.
|
31
58
|
|
32
59
|
Here's a sample usage in a Rails app:
|
33
60
|
|
@@ -35,22 +62,19 @@ Here's a sample usage in a Rails app:
|
|
35
62
|
def enter
|
36
63
|
@room = Room.find_by_id(params[:id])
|
37
64
|
|
38
|
-
mutex = Redis::
|
39
|
-
|
40
|
-
if mutex.lock # return true when you successfully obtain the lock for the room 123
|
41
|
-
...
|
65
|
+
mutex = Redis::Mutex.new(@room) # key => "Room:123"
|
66
|
+
mutex.lock do
|
42
67
|
do_something
|
43
|
-
...
|
44
|
-
mutex.unlock # exit the critical section ASAP
|
45
68
|
end
|
46
69
|
end
|
47
70
|
end
|
48
71
|
|
49
|
-
|
72
|
+
Note that you need to explicitly call the unlock method unless you don't use the block syntax.
|
50
73
|
|
51
|
-
|
74
|
+
Also note that, if you take a closer look, you find that the actual key is structured in the following form:
|
52
75
|
|
53
|
-
Redis
|
54
|
-
=> ["Redis::
|
76
|
+
Redis.new.keys
|
77
|
+
=> ["Redis::Mutex:Room:123"]
|
55
78
|
|
79
|
+
The automatic prefixing and binding is the feature of Redis::Classy.
|
56
80
|
For more internal details, refer to https://github.com/kenn/redis-classy
|
data/Rakefile
CHANGED
@@ -27,9 +27,12 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
27
27
|
spec.pattern = FileList['spec/**/*_spec.rb']
|
28
28
|
end
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
desc "Flush the test database"
|
31
|
+
task :flushdb do
|
32
|
+
require 'redis-classy'
|
33
|
+
Redis::Classy.db = Redis.new
|
34
|
+
Redis::Classy.select 1
|
35
|
+
Redis::Classy.flushdb
|
33
36
|
end
|
34
37
|
|
35
38
|
task :default => :spec
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/lib/redis/mutex.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
class Redis
|
2
|
+
#
|
3
|
+
# Redis::Mutex 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. (in seconds, default: 10)
|
11
|
+
#
|
12
|
+
class Mutex < Redis::Classy
|
13
|
+
|
14
|
+
DEFAULT_EXPIRE = 10
|
15
|
+
attr_accessor :options
|
16
|
+
|
17
|
+
def initialize(object, options={})
|
18
|
+
super(object.is_a?(String) || object.is_a?(Symbol) ? object : "#{object.class.name}:#{object.id}")
|
19
|
+
@options = options
|
20
|
+
@options[:block] ||= 1
|
21
|
+
@options[:sleep] ||= 0.1
|
22
|
+
@options[:expire] ||= DEFAULT_EXPIRE
|
23
|
+
end
|
24
|
+
|
25
|
+
def lock
|
26
|
+
if @options[:block] > 0
|
27
|
+
start_at = Time.now
|
28
|
+
success = false
|
29
|
+
while Time.now - start_at < @options[:block]
|
30
|
+
success = true and break if try_lock
|
31
|
+
sleep @options[:sleep]
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# Non-blocking
|
35
|
+
success = try_lock
|
36
|
+
end
|
37
|
+
|
38
|
+
if block_given? and success
|
39
|
+
yield
|
40
|
+
# Since it's possible that the yielded operation took a long time, we can't just simply
|
41
|
+
# Release the lock. The unlock method checks if the expires_at remains the same that you
|
42
|
+
# set, and do not release it when the lock timestamp was overwritten.
|
43
|
+
unlock
|
44
|
+
end
|
45
|
+
|
46
|
+
success
|
47
|
+
end
|
48
|
+
|
49
|
+
def try_lock
|
50
|
+
now = Time.now.to_f
|
51
|
+
@expires_at = now + @options[:expire] # Extend in each blocking loop
|
52
|
+
return true if setnx(@expires_at) # Success, the lock has been acquired
|
53
|
+
return false if get.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
|
+
def unlock(force=false)
|
61
|
+
del if get.to_f == @expires_at or force # Release the lock if it seems to be yours
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.sweep
|
65
|
+
return 0 if (all_keys = keys).empty?
|
66
|
+
|
67
|
+
now = Time.now.to_f
|
68
|
+
values = mget(*all_keys)
|
69
|
+
|
70
|
+
expired_keys = [].tap do |array|
|
71
|
+
all_keys.each_with_index do |key, i|
|
72
|
+
array << key if !values[i].nil? and values[i].to_f <= now
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
expired_keys.each do |key|
|
77
|
+
del(key) if getset(key, now + DEFAULT_EXPIRE).to_f <= now # Make extra sure that anyone haven't extended the lock
|
78
|
+
end
|
79
|
+
|
80
|
+
expired_keys.size
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/redis-mutex.rb
CHANGED
data/redis-mutex.gemspec
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{redis-mutex}
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Kenn Ejima"]
|
12
|
+
s.date = %q{2011-03-01}
|
13
|
+
s.description = %q{Distrubuted non-blocking mutex in Ruby using Redis}
|
14
|
+
s.email = %q{kenn.ejima@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rspec",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/redis-mutex.rb",
|
29
|
+
"lib/redis/mutex.rb",
|
30
|
+
"redis-mutex.gemspec",
|
31
|
+
"spec/redis_mutex_spec.rb",
|
32
|
+
"spec/spec_helper.rb"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/kenn/redis-mutex}
|
35
|
+
s.licenses = ["MIT"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.5.3}
|
38
|
+
s.summary = %q{Distrubuted non-blocking mutex in Ruby using Redis}
|
39
|
+
s.test_files = [
|
40
|
+
"spec/redis_mutex_spec.rb",
|
41
|
+
"spec/spec_helper.rb"
|
42
|
+
]
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
s.specification_version = 3
|
46
|
+
|
47
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
48
|
+
s.add_runtime_dependency(%q<redis-classy>, ["~> 0.9.0"])
|
49
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.5.0"])
|
50
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
51
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<redis-classy>, ["~> 0.9.0"])
|
54
|
+
s.add_dependency(%q<rspec>, ["~> 2.5.0"])
|
55
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
56
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
57
|
+
end
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<redis-classy>, ["~> 0.9.0"])
|
60
|
+
s.add_dependency(%q<rspec>, ["~> 2.5.0"])
|
61
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
62
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Redis::Mutex do
|
4
|
+
after do
|
5
|
+
Redis::Classy.flushdb
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should set the value to the expiration" do
|
9
|
+
start = Time.now
|
10
|
+
expires_in = 10
|
11
|
+
mutex = Redis::Mutex.new(:test_lock, :expire => expires_in)
|
12
|
+
mutex.lock do
|
13
|
+
mutex.get.to_f.should be_within(1.0).of((start + expires_in).to_f)
|
14
|
+
end
|
15
|
+
# key should have been cleaned up
|
16
|
+
mutex.get.should be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should get a lock when existing lock is expired" do
|
20
|
+
mutex = Redis::Mutex.new(:test_lock)
|
21
|
+
# locked in the far past
|
22
|
+
Redis::Mutex.set(:test_lock, Time.now - 60)
|
23
|
+
|
24
|
+
mutex.lock.should be_true
|
25
|
+
mutex.get.should_not be_nil
|
26
|
+
mutex.unlock
|
27
|
+
mutex.get.should be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should not get a lock when existing lock is still effective" do
|
31
|
+
mutex = Redis::Mutex.new(:test_lock, :block => 0.2)
|
32
|
+
|
33
|
+
# someone beats us to it
|
34
|
+
mutex2 = Redis::Mutex.new(:test_lock, :block => 0.2)
|
35
|
+
mutex2.lock
|
36
|
+
|
37
|
+
mutex.lock.should be_false # should not have the lock
|
38
|
+
mutex.get.should_not be_nil # lock value should still be set
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should not remove the key if lock is held past expiration" do
|
42
|
+
mutex = Redis::Mutex.new(:test_lock, :expire => 0.1, :block => 0)
|
43
|
+
mutex.lock
|
44
|
+
sleep 0.2 # lock expired
|
45
|
+
|
46
|
+
# someone overwrites the expired lock
|
47
|
+
mutex2 = Redis::Mutex.new(:test_lock, :expire => 10, :block => 0)
|
48
|
+
mutex2.lock.should be_true
|
49
|
+
|
50
|
+
mutex.unlock
|
51
|
+
mutex.get.should_not be_nil # lock should still be there
|
52
|
+
end
|
53
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -7,6 +7,12 @@ require 'redis-mutex'
|
|
7
7
|
# in ./support/ and its subdirectories.
|
8
8
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
9
|
|
10
|
+
Redis::Classy.db = Redis.new
|
11
|
+
Redis::Classy.select 1
|
12
|
+
unless Redis::Classy.keys.empty?
|
13
|
+
puts '[ERROR]: Redis database 1 not empty! run "rake flushdb" beforehand.'
|
14
|
+
exit!
|
15
|
+
end
|
16
|
+
|
10
17
|
RSpec.configure do |config|
|
11
|
-
|
12
18
|
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: redis-mutex
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 1.0.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Kenn Ejima
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-
|
13
|
+
date: 2011-03-01 00:00:00 -08:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -76,8 +76,9 @@ files:
|
|
76
76
|
- Rakefile
|
77
77
|
- VERSION
|
78
78
|
- lib/redis-mutex.rb
|
79
|
-
- lib/redis/
|
80
|
-
-
|
79
|
+
- lib/redis/mutex.rb
|
80
|
+
- redis-mutex.gemspec
|
81
|
+
- spec/redis_mutex_spec.rb
|
81
82
|
- spec/spec_helper.rb
|
82
83
|
has_rdoc: true
|
83
84
|
homepage: http://github.com/kenn/redis-mutex
|
@@ -93,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
94
|
requirements:
|
94
95
|
- - ">="
|
95
96
|
- !ruby/object:Gem::Version
|
96
|
-
hash:
|
97
|
+
hash: -1194994369925945289
|
97
98
|
segments:
|
98
99
|
- 0
|
99
100
|
version: "0"
|
@@ -111,5 +112,5 @@ signing_key:
|
|
111
112
|
specification_version: 3
|
112
113
|
summary: Distrubuted non-blocking mutex in Ruby using Redis
|
113
114
|
test_files:
|
114
|
-
- spec/
|
115
|
+
- spec/redis_mutex_spec.rb
|
115
116
|
- spec/spec_helper.rb
|
data/lib/redis/classy/mutex.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
class Redis::Classy::Mutex < Redis::Classy
|
2
|
-
|
3
|
-
TIMEOUT = 10
|
4
|
-
|
5
|
-
def initialize(object, timeout=TIMEOUT)
|
6
|
-
@now = Time.now.to_i
|
7
|
-
@expires_at = @now + timeout
|
8
|
-
super(object.is_a?(String) ? object : "#{object.class.name}:#{object.id}")
|
9
|
-
end
|
10
|
-
|
11
|
-
def lock
|
12
|
-
return true if self.setnx(@expires_at) # Success, the lock was acquired
|
13
|
-
return false if self.get.to_i > @now # Failure, someone took the lock and it is still effective
|
14
|
-
|
15
|
-
# The lock has expired but wasn't released... BAD!
|
16
|
-
return true if self.getset(@expires_at).to_i <= @now # Success, we acquired the previously expired lock!
|
17
|
-
return false # Dammit, it seems that someone else was even faster than us to acquire this lock.
|
18
|
-
end
|
19
|
-
|
20
|
-
def unlock
|
21
|
-
self.del if self.get.to_i == @expires_at # Release the lock if it seems to be yours.
|
22
|
-
true
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.sweep(timeout=TIMEOUT)
|
26
|
-
now = Time.now.to_i
|
27
|
-
keys = self.keys
|
28
|
-
|
29
|
-
return 0 if keys.empty?
|
30
|
-
|
31
|
-
values = self.mget(*keys)
|
32
|
-
|
33
|
-
expired_keys = [].tap do |array|
|
34
|
-
keys.each_with_index do |key, i|
|
35
|
-
array << key if !values[i].nil? and values[i].to_i <= now
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
expired_keys.each do |key|
|
40
|
-
self.del(key) if self.getset(key, now + timeout).to_i <= now # Make extra sure someone haven't released the lock yet.
|
41
|
-
end
|
42
|
-
|
43
|
-
expired_keys.size
|
44
|
-
end
|
45
|
-
end
|