Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Version bump to 1.2.0

  • Loading branch information...
commit 2302e7270446088eecc6615971f2ad38ecd9c043 1 parent 8559b05
@kenn authored
View
38 README.md
@@ -1,7 +1,7 @@
Redis Mutex
===========
-Distrubuted mutex in Ruby using Redis. Supports both blocking and non-blocking semantics.
+Distrubuted mutex in Ruby using Redis. Supports both **blocking** and **non-blocking** semantics.
The idea was taken from [the official SETNX doc](http://redis.io/commands/setnx).
@@ -13,14 +13,14 @@ In the following example, only one thread / process / server can enter the locke
```ruby
mutex = Redis::Mutex.new(:your_lock_name)
mutex.lock do
- do_something
+ # do something exclusively
end
```
By default, when one is holding a lock, others wait **1 second** in total, polling **every 100ms** to see if the lock was released.
When 1 second has passed, the lock method returns `false`.
-If you want to immediately receive `false` on an unsuccessful locking attempt, you can configure the mutex to work in the non-blocking mode.
+If you want to immediately receive `false` on an unsuccessful locking attempt, you can configure the mutex to work in the non-blocking mode, as explained later.
Install
-------
@@ -59,27 +59,27 @@ or pass any string or symbol.
Also the initialize method takes several options.
```ruby
-:block => 1 # Specify in seconds how long you want to wait for the lock to be released. Speficy 0
- # if you need non-blocking sematics and return false immediately. (default: 1)
+:block => 1 # Specify in seconds how long you want to wait for the lock to be released.
+ # Speficy 0 if you need non-blocking sematics and return false immediately. (default: 1)
:sleep => 0.1 # Specify in seconds how long the polling interval should be when :block is given.
# It is recommended that you do NOT go below 0.01. (default: 0.1)
:expire => 10 # Specify in seconds when the lock should forcibly be removed when something went wrong
- # with the one who held the lock. (in seconds, default: 10)
+ # with the one who held the lock. (default: 10)
```
-The lock method returns true when the lock has been successfully obtained, or returns false when the attempts
-failed after the seconds specified with :block. It immediately returns false when 0 is given to :block.
+The lock method returns `true` when the lock has been successfully obtained, or returns `false` when the attempts
+failed after the seconds specified with **:block**. It immediately returns `false` when 0 is given to **:block**.
Here's a sample usage in a Rails app:
```ruby
class RoomController < ApplicationController
def enter
- @room = Room.find_by_id(params[:id])
+ @room = Room.find(params[:id])
mutex = Redis::Mutex.new(@room) # key => "Room:123"
mutex.lock do
- do_something
+ # do something exclusively
end
end
end
@@ -87,11 +87,25 @@ end
Note that you need to explicitly call the unlock method unless you don't use the block syntax.
-Also note that, if you take a closer look, you find that the actual key is structured in the following form:
+In the following example,
+
+```ruby
+def enter
+ mutex = Redis::Mutex.new('non-blocking', :block => 0, :expire => 10.minutes)
+ mutex.lock
+ # do something exclusively
+ mutex.unlock
+rescue
+ mutex.unlock
+ raise
+end
+```
+
+Also note that, internally, the actual key is structured in the following form:
```ruby
Redis.new.keys
- => ["Redis::Mutex:Room:123"]
+ => ["Redis::Mutex:Room:111", "Redis::Mutex:Room:112", ... ]
```
The automatic prefixing and binding is the feature of `Redis::Classy`.
View
6 Rakefile
@@ -15,8 +15,8 @@ Jeweler::Tasks.new do |gem|
gem.name = "redis-mutex"
gem.homepage = "http://github.com/kenn/redis-mutex"
gem.license = "MIT"
- gem.summary = "Distrubuted non-blocking and blocking mutex using Redis"
- gem.description = "Distrubuted non-blocking and blocking mutex using Redis"
+ gem.summary = "Distrubuted mutex using Redis"
+ gem.description = "Distrubuted mutex using Redis"
gem.email = "kenn.ejima@gmail.com"
gem.authors = ["Kenn Ejima"]
end
@@ -31,5 +31,5 @@ end
task :default => :spec
task :spec do
- exec "rspec spec/redis_mutex_spec.rb"
+ exec "rspec spec"
end
View
2  VERSION
@@ -1 +1 @@
-1.1.0
+1.2.0
View
78 lib/redis/mutex.rb
@@ -1,46 +1,51 @@
class Redis
#
- # Redis::Mutex options
+ # Options
#
# :block => Specify in seconds how long you want to wait for the lock to be released. Speficy 0
# if you need non-blocking sematics and return false immediately. (default: 1)
# :sleep => Specify in seconds how long the polling interval should be when :block is given.
# It is recommended that you do NOT go below 0.01. (default: 0.1)
# :expire => Specify in seconds when the lock should forcibly be removed when something went wrong
- # with the one who held the lock. (in seconds, default: 10)
+ # with the one who held the lock. (default: 10)
#
class Mutex < Redis::Classy
-
+ autoload :Macro, 'redis/mutex/macro'
+ attr_reader :block, :sleep, :expire, :locking
DEFAULT_EXPIRE = 10
- attr_accessor :options
def initialize(object, options={})
super(object.is_a?(String) || object.is_a?(Symbol) ? object : "#{object.class.name}:#{object.id}")
- @options = options
- @options[:block] ||= 1
- @options[:sleep] ||= 0.1
- @options[:expire] ||= DEFAULT_EXPIRE
+ @block = options[:block] || 1
+ @sleep = options[:sleep] || 0.1
+ @expire = options[:expire] || DEFAULT_EXPIRE
end
def lock
- if @options[:block] > 0
+ @locking = false
+
+ if @block > 0
+ # Blocking mode
start_at = Time.now
- success = false
- while Time.now - start_at < @options[:block]
- success = true and break if try_lock
- sleep @options[:sleep]
+ while Time.now - start_at < @block
+ @locking = true and break if try_lock
+ Kernel.sleep @sleep
end
else
- # Non-blocking
- success = try_lock
+ # Non-blocking mode
+ @locking = try_lock
end
+ success = @locking # Backup
- if block_given? and success
- yield
- # Since it's possible that the yielded operation took a long time, we can't just simply
- # Release the lock. The unlock method checks if the expires_at remains the same that you
- # set, and do not release it when the lock timestamp was overwritten.
- unlock
+ if block_given? and @locking
+ begin
+ yield
+ ensure
+ # Since it's possible that the yielded operation took a long time, we can't just simply
+ # Release the lock. The unlock method checks if the expires_at remains the same that you
+ # set, and do not release it when the lock timestamp was overwritten.
+ unlock
+ end
end
success
@@ -48,7 +53,7 @@ def lock
def try_lock
now = Time.now.to_f
- @expires_at = now + @options[:expire] # Extend in each blocking loop
+ @expires_at = now + @expire # Extend in each blocking loop
return true if setnx(@expires_at) # Success, the lock has been acquired
return false if get.to_f > now # Check if the lock is still effective
@@ -58,26 +63,33 @@ def try_lock
end
def unlock(force=false)
+ @locking = false
del if get.to_f == @expires_at or force # Release the lock if it seems to be yours
end
- def self.sweep
- return 0 if (all_keys = keys).empty?
+ class << self
+ def sweep
+ return 0 if (all_keys = keys).empty?
- now = Time.now.to_f
- values = mget(*all_keys)
+ now = Time.now.to_f
+ values = mget(*all_keys)
- expired_keys = [].tap do |array|
- all_keys.each_with_index do |key, i|
- array << key if !values[i].nil? and values[i].to_f <= now
+ expired_keys = [].tap do |array|
+ all_keys.each_with_index do |key, i|
+ array << key if !values[i].nil? and values[i].to_f <= now
+ end
+ end
+
+ expired_keys.each do |key|
+ del(key) if getset(key, now + DEFAULT_EXPIRE).to_f <= now # Make extra sure that anyone haven't extended the lock
end
- end
- expired_keys.each do |key|
- del(key) if getset(key, now + DEFAULT_EXPIRE).to_f <= now # Make extra sure that anyone haven't extended the lock
+ expired_keys.size
end
- expired_keys.size
+ def lock(object, options={}, &block)
+ new(object, options).lock(&block)
+ end
end
end
end
View
41 lib/redis/mutex/macro.rb
@@ -0,0 +1,41 @@
+class Redis
+ class Mutex
+ module Macro
+ def self.included(base)
+ base.extend ClassMethods
+ base.class_eval do
+ class << self
+ attr_accessor :auto_mutex_methods
+ end
+ @auto_mutex_methods = {}
+ end
+ end
+
+ module ClassMethods
+ def auto_mutex(target, options={})
+ self.auto_mutex_methods[target] = options
+ end
+
+ def method_added(target)
+ return if target.to_s =~ /^auto_mutex_methods.*$/
+ return unless self.auto_mutex_methods[target]
+ without_method = "#{target}_without_auto_mutex"
+ with_method = "#{target}_with_auto_mutex"
+ return if method_defined?(without_method)
+
+ define_method(with_method) do |*args|
+ key = self.class.name << '#' << target.to_s
+ options = self.class.auto_mutex_methods[target]
+
+ Redis::Mutex.lock(key, options) do
+ send(without_method, *args)
+ end
+ end
+
+ alias_method without_method, target
+ alias_method target, with_method
+ end
+ end
+ end
+ end
+end
View
40 redis-mutex.gemspec
@@ -4,17 +4,17 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
- s.name = %q{redis-mutex}
+ s.name = "redis-mutex"
s.version = "1.1.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Kenn Ejima"]
- s.date = %q{2011-05-21}
- s.description = %q{Distrubuted non-blocking mutex in Ruby using Redis}
- s.email = %q{kenn.ejima@gmail.com}
+ s.date = "2011-11-05"
+ s.description = "Distrubuted mutex using Redis"
+ s.email = "kenn.ejima@gmail.com"
s.extra_rdoc_files = [
"LICENSE.txt",
- "README.rdoc"
+ "README.md"
]
s.files = [
".document",
@@ -22,40 +22,44 @@ Gem::Specification.new do |s|
"Gemfile",
"Gemfile.lock",
"LICENSE.txt",
- "README.rdoc",
+ "README.md",
"Rakefile",
"VERSION",
"lib/redis-mutex.rb",
"lib/redis/mutex.rb",
+ "lib/redis/mutex/macro.rb",
"redis-mutex.gemspec",
"spec/redis_mutex_spec.rb",
"spec/spec_helper.rb"
]
- s.homepage = %q{http://github.com/kenn/redis-mutex}
+ s.homepage = "http://github.com/kenn/redis-mutex"
s.licenses = ["MIT"]
s.require_paths = ["lib"]
- s.rubygems_version = %q{1.6.2}
- s.summary = %q{Distrubuted non-blocking mutex in Ruby using Redis}
+ s.rubygems_version = "1.8.11"
+ s.summary = "Distrubuted mutex using Redis"
if s.respond_to? :specification_version then
s.specification_version = 3
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<redis-classy>, ["~> 1.0.0"])
- s.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
- s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
- s.add_development_dependency(%q<jeweler>, ["~> 1.6.0"])
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0"])
+ s.add_development_dependency(%q<rspec>, [">= 0"])
+ s.add_development_dependency(%q<bundler>, [">= 0"])
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
else
s.add_dependency(%q<redis-classy>, ["~> 1.0.0"])
- s.add_dependency(%q<rspec>, ["~> 2.6.0"])
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
- s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
+ s.add_dependency(%q<rspec>, [">= 0"])
+ s.add_dependency(%q<bundler>, [">= 0"])
+ s.add_dependency(%q<jeweler>, [">= 0"])
end
else
s.add_dependency(%q<redis-classy>, ["~> 1.0.0"])
- s.add_dependency(%q<rspec>, ["~> 2.6.0"])
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
- s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
+ s.add_dependency(%q<rspec>, [">= 0"])
+ s.add_dependency(%q<bundler>, [">= 0"])
+ s.add_dependency(%q<jeweler>, [">= 0"])
end
end
View
48 spec/redis_mutex_spec.rb
@@ -3,6 +3,7 @@
describe Redis::Mutex do
before do
Redis::Classy.flushdb
+ @short_mutex_options = { :block => 0.1, :sleep => 0.02 }
end
after do
@@ -32,10 +33,10 @@
end
it "should not get a lock when existing lock is still effective" do
- mutex = Redis::Mutex.new(:test_lock, :block => 0.2)
+ mutex = Redis::Mutex.new(:test_lock, @short_mutex_options)
# someone beats us to it
- mutex2 = Redis::Mutex.new(:test_lock, :block => 0.2)
+ mutex2 = Redis::Mutex.new(:test_lock, @short_mutex_options)
mutex2.lock
mutex.lock.should be_false # should not have the lock
@@ -54,4 +55,47 @@
mutex.unlock
mutex.get.should_not be_nil # lock should still be there
end
+
+ it "should ensure unlock when something goes wrong in the block" do
+ mutex = Redis::Mutex.new(:test_lock)
+ begin
+ mutex.lock do
+ raise "Something went wrong!"
+ end
+ rescue
+ mutex.locking.should be_false
+ end
+ end
+
+ it "should reset locking state on reuse" do
+ mutex = Redis::Mutex.new(:test_lock, @short_mutex_options)
+ mutex.lock.should be_true
+ mutex.lock.should be_false
+ end
+
+ describe Redis::Mutex::Macro do
+ it "should add auto_mutex" do
+
+ class C
+ include Redis::Mutex::Macro
+ auto_mutex :run_singularly, :block => 0 # Give up immediately if lock is taken
+ @@result = 0
+
+ def run_singularly
+ sleep 0.1
+ Thread.exclusive { @@result += 1 }
+ end
+
+ def self.result
+ @@result
+ end
+ end
+
+ t1 = Thread.new { C.new.run_singularly }
+ t2 = Thread.new { C.new.run_singularly }
+ t1.join
+ t2.join
+ C.result.should == 1
+ end
+ end
end
Please sign in to comment.
Something went wrong with that request. Please try again.