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 CHANGED
@@ -1,11 +1,25 @@
1
1
  = Redis Mutex
2
2
 
3
- Distrubuted non-blocking mutex in Ruby using Redis.
3
+ Distrubuted mutex in Ruby using Redis. Supports both blocking and non-blocking semantics.
4
4
 
5
- The idea was taken from - http://redis.io/commands/setnx and http://gist.github.com/457269
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
- In config/initializers/redis_mutex.rb:
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::Classy::Mutex.new(some_object)
39
+ mutex = Redis::Mutex.new(key, options)
26
40
  mutex.lock
27
41
  mutex.unlock
28
- Redis::Classy::Mutex.sweep
42
+ Redis::Mutex.sweep
29
43
 
30
- It takes any Ruby objects that respond to :id, where the key is automatically set as "TheClass:id", or pass any string as a key.
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::Classy::Mutex.new(@room) # key => "Room:123"
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
- The caveat is that the lock just returns false when it fails and do not block.
72
+ Note that you need to explicitly call the unlock method unless you don't use the block syntax.
50
73
 
51
- If you take a closer look, you find that the actual key is structured in the following form:
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::Classy.keys
54
- => ["Redis::Classy::Mutex:Room:123"]
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
- RSpec::Core::RakeTask.new(:rcov) do |spec|
31
- spec.pattern = 'spec/**/*_spec.rb'
32
- spec.rcov = true
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.9.0
1
+ 1.0.0
@@ -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
@@ -1,5 +1,5 @@
1
1
  require 'redis-classy'
2
2
 
3
- class Redis::Classy
4
- autoload :Mutex, 'redis/classy/mutex'
3
+ class Redis
4
+ autoload :Mutex, 'redis/mutex'
5
5
  end
@@ -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.9.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-02-28 00:00:00 -08:00
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/classy/mutex.rb
80
- - spec/redis-mutex_spec.rb
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: 4175039287226783610
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/redis-mutex_spec.rb
115
+ - spec/redis_mutex_spec.rb
115
116
  - spec/spec_helper.rb
@@ -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
@@ -1,7 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
-
3
- describe "RedisMutex" do
4
- pending "fails" do
5
- fail "hey buddy, you should probably rename this file and start specing for real"
6
- end
7
- end