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 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