jashmenn-redis-lock 0.1.1
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.md +47 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/redis-lock.rb +5 -0
- data/lib/redis/lock.rb +93 -0
- data/spec/redis_spec.rb +60 -0
- data/spec/spec_helper.rb +4 -0
- metadata +70 -0
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
redis-lock
|
2
|
+
==========
|
3
|
+
|
4
|
+
Requires the redis gem. Including this in your project will give you additional locking abilities on any instance of a redis connection you create.
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
gem install redis-lock
|
10
|
+
|
11
|
+
Usage
|
12
|
+
-----
|
13
|
+
|
14
|
+
require 'redis'
|
15
|
+
require 'redis-lock # This will automatically include Lock into the Redis class.
|
16
|
+
|
17
|
+
Here's a little example of what you can do with it:
|
18
|
+
|
19
|
+
timeout = 10 # measured in seconds
|
20
|
+
max_attempts = 100 # number of times the action will attempt to lock the key before raising an exception
|
21
|
+
|
22
|
+
$redis = Redis.new
|
23
|
+
|
24
|
+
$redis.lock('beers_on_the_wall', timeout, max_attempts)
|
25
|
+
# Now no one can acquire a lock on 'beers_on_the_wall'
|
26
|
+
|
27
|
+
$redis.unlock('beers_on_the_wall')
|
28
|
+
# Other processes can now acquire a lock on 'beers_on_the_wall'
|
29
|
+
|
30
|
+
For convenience, there is also a `lock_with_update` function that accepts a block. It handles the locking and unlocking for you.
|
31
|
+
|
32
|
+
$redis.lock_for_update('beers_on_the_wall') do
|
33
|
+
$redis.multi do
|
34
|
+
$redis.set('sing', 'take one down, pass it around.')
|
35
|
+
$redis.decr('beers_on_the_wall')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Additional Notes
|
40
|
+
----------------
|
41
|
+
|
42
|
+
This gem basically implements the algorithm described here: http://redis.io/commands/setnx
|
43
|
+
|
44
|
+
Author
|
45
|
+
------
|
46
|
+
|
47
|
+
Patrick Tulskie; http://patricktulskie.com
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "redis-lock"
|
8
|
+
gem.summary = %Q{TODO: one-line summary of your gem}
|
9
|
+
gem.description = %Q{TODO: longer description of your gem}
|
10
|
+
gem.email = "patricktulskie@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/PatrickTulskie/redis-lock"
|
12
|
+
gem.authors = ["Patrick Tulskie"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "redis-lock #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/redis-lock.rb
ADDED
data/lib/redis/lock.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Lock
|
5
|
+
|
6
|
+
# Lock a given key for updating
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# $redis = Redis.new
|
11
|
+
# lock_for_update('beers_on_the_wall', 20, 1000) do
|
12
|
+
# $redis.decr('beers_on_the_wall')
|
13
|
+
# end
|
14
|
+
|
15
|
+
def lock_for_update(key, timeout = 60, max_attempts = 100)
|
16
|
+
if self.lock(key, timeout, max_attempts)
|
17
|
+
begin
|
18
|
+
response = yield if block_given?
|
19
|
+
ensure
|
20
|
+
self.unlock(key)
|
21
|
+
end
|
22
|
+
return response
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Lock a given key. Optionally takes a timeout and max number of attempts to lock the key before giving up.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
#
|
30
|
+
# $redis.lock('beers_on_the_wall', 10, 100)
|
31
|
+
|
32
|
+
def lock(key, timeout = 60, max_attempts = 100)
|
33
|
+
current_lock_key = lock_key(key)
|
34
|
+
expiration_value = lock_expiration(timeout)
|
35
|
+
attempt_counter = 0
|
36
|
+
begin
|
37
|
+
if self.setnx(current_lock_key, expiration_value)
|
38
|
+
return true
|
39
|
+
else
|
40
|
+
current_lock = self.get(current_lock_key)
|
41
|
+
if (current_lock.to_s.split('-').first.to_i) < Time.now.to_i
|
42
|
+
compare_value = self.getset(current_lock_key, expiration_value)
|
43
|
+
return true if compare_value == current_lock
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
raise "Unable to acquire lock for #{key}."
|
48
|
+
rescue => e
|
49
|
+
if e.message == "Unable to acquire lock for #{key}."
|
50
|
+
if attempt_counter == max_attempts
|
51
|
+
raise
|
52
|
+
else
|
53
|
+
attempt_counter += 1
|
54
|
+
sleep 1
|
55
|
+
retry
|
56
|
+
end
|
57
|
+
else
|
58
|
+
raise
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Unlock a previously locked key if it has not expired and the current process was the one that locked it.
|
64
|
+
#
|
65
|
+
# Example:
|
66
|
+
#
|
67
|
+
# $redis.unlock('beers_on_the_wall')
|
68
|
+
|
69
|
+
def unlock(key)
|
70
|
+
current_lock_key = lock_key(key)
|
71
|
+
lock_value = self.get(current_lock_key)
|
72
|
+
return true unless lock_value
|
73
|
+
lock_timeout, lock_holder = lock_value.split('-')
|
74
|
+
if (lock_timeout.to_i > Time.now.to_i) && (lock_holder.to_i == Process.pid)
|
75
|
+
self.del(current_lock_key)
|
76
|
+
return true
|
77
|
+
else
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def lock_expiration(timeout)
|
85
|
+
"#{Time.now.to_i + timeout + 1}-#{Process.pid}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def lock_key(key)
|
89
|
+
"lock:#{key}"
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
data/spec/redis_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'redis-lock'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
describe 'redis' do
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
@redis = Redis.new
|
9
|
+
end
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
@redis.flushdb
|
13
|
+
end
|
14
|
+
|
15
|
+
after(:each) do
|
16
|
+
@redis.flushdb
|
17
|
+
end
|
18
|
+
|
19
|
+
after(:all) do
|
20
|
+
@redis.quit
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should respond to lock" do
|
24
|
+
@redis.should respond_to(:lock)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should respond to unlock" do
|
28
|
+
@redis.should respond_to(:unlock)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should respond to lock_for_update" do
|
32
|
+
@redis.should respond_to(:lock_for_update)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should lock a key" do
|
36
|
+
@redis.lock('test_key').should be_true
|
37
|
+
@redis.get('lock:test_key').should_not be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should unlock a key" do
|
41
|
+
@redis.lock('test_key').should be_true
|
42
|
+
@redis.unlock('test_key').should be_true
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should raise an exception if unable to acquire lock" do
|
46
|
+
@redis.lock('test_key', 9000)
|
47
|
+
lambda { @redis.lock('test_key', 9000, 1) }.should raise_exception("Unable to acquire lock for test_key.")
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should execute a block during a lock_for_update transaction" do
|
51
|
+
@redis.lock_for_update('test_key', 9000) { @redis.set('test_key', 'awesome') }
|
52
|
+
@redis.get('test_key').should == 'awesome'
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should unlock at the end of a lock_for_update" do
|
56
|
+
@redis.lock_for_update('test_key', 9000) { @redis.set('test_key', 'awesome') }
|
57
|
+
@redis.get('lock:test_key').should be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jashmenn-redis-lock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Patrick Tulskie
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-01-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &70214692078260 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70214692078260
|
25
|
+
description: ! " Adds pessimistic locking capabilities to the redis gem.\n \n Since
|
26
|
+
these capabilities are utilized client-side, all clients must use this gem and follow
|
27
|
+
the order of lock => make changes => unlock in order to obtain maximum safety when
|
28
|
+
modifying sensitive keys.\n \n Tested with redis-server 2.0.4 and should work
|
29
|
+
with all versions > 0.091.\n"
|
30
|
+
email: patricktulskie@gmail.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files:
|
34
|
+
- README.md
|
35
|
+
files:
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- VERSION
|
39
|
+
- lib/redis-lock.rb
|
40
|
+
- lib/redis/lock.rb
|
41
|
+
- spec/redis_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
homepage: http://github.com/PatrickTulskie/redis-lock
|
44
|
+
licenses: []
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --charset=UTF-8
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.8.17
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Adds the ability to utilize client-side pessimistic locking in Redis.
|
68
|
+
test_files:
|
69
|
+
- spec/redis_spec.rb
|
70
|
+
- spec/spec_helper.rb
|