redis-mutex 0.9.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|