redis-lock 0.1.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.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
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/../lib/redis/lock'
2
+
3
+ class Redis
4
+ include Redis::Lock
5
+ end
data/lib/redis/lock.rb ADDED
@@ -0,0 +1,90 @@
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
+ response = yield if block_given?
18
+ self.unlock(key)
19
+ return response
20
+ end
21
+ end
22
+
23
+ # Lock a given key. Optionally takes a timeout and max number of attempts to lock the key before giving up.
24
+ #
25
+ # Example:
26
+ #
27
+ # $redis.lock('beers_on_the_wall', 10, 100)
28
+
29
+ def lock(key, timeout = 60, max_attempts = 100)
30
+ current_lock_key = lock_key(key)
31
+ expiration_value = lock_expiration(timeout)
32
+ attempt_counter = 0
33
+ begin
34
+ if self.setnx(current_lock_key, expiration_value)
35
+ return true
36
+ else
37
+ current_lock = self.get(current_lock_key)
38
+ if (current_lock.to_s.split('-').first.to_i) < Time.now.to_i
39
+ compare_value = self.getset(current_lock_key, expiration_value)
40
+ return true if compare_value == current_lock
41
+ end
42
+ end
43
+
44
+ raise "Unable to acquire lock for #{key}."
45
+ rescue => e
46
+ if e.message == "Unable to acquire lock for #{key}."
47
+ if attempt_counter == max_attempts
48
+ raise
49
+ else
50
+ attempt_counter += 1
51
+ sleep 1
52
+ retry
53
+ end
54
+ else
55
+ raise
56
+ end
57
+ end
58
+ end
59
+
60
+ # Unlock a previously locked key if it has not expired and the current process was the one that locked it.
61
+ #
62
+ # Example:
63
+ #
64
+ # $redis.unlock('beers_on_the_wall')
65
+
66
+ def unlock(key)
67
+ current_lock_key = lock_key(key)
68
+ lock_value = self.get(current_lock_key)
69
+ return true unless lock_value
70
+ lock_timeout, lock_holder = lock_value.split('-')
71
+ if (lock_timeout.to_i > Time.now.to_i) && (lock_holder.to_i == Process.pid)
72
+ self.del(current_lock_key)
73
+ return true
74
+ else
75
+ return false
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def lock_expiration(timeout)
82
+ "#{Time.now.to_i + timeout + 1}-#{Process.pid}"
83
+ end
84
+
85
+ def lock_key(key)
86
+ "lock:#{key}"
87
+ end
88
+
89
+ end
90
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'redis'
3
+ $TESTING = true
4
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-lock
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Patrick Tulskie
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-16 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: redis
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: " Adds pessimistic locking capabilities to the redis gem.\n \n Since these capabilities are utilized client-side, all clients must use this gem and follow the order of lock => make changes => unlock in order to obtain maximum safety when modifying sensitive keys.\n \n Tested with redis-server 2.0.4 and should work with all versions > 0.091.\n"
36
+ email: patricktulskie@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.md
43
+ files:
44
+ - README.md
45
+ - Rakefile
46
+ - VERSION
47
+ - lib/redis-lock.rb
48
+ - lib/redis/lock.rb
49
+ - spec/redis_spec.rb
50
+ - spec/spec_helper.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/PatrickTulskie/redis-lock
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --charset=UTF-8
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.4.1
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Adds the ability to utilize client-side pessimistic locking in Redis.
85
+ test_files:
86
+ - spec/redis_spec.rb
87
+ - spec/spec_helper.rb