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.
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/../lib/redis/lock'
2
+
3
+ class Redis
4
+ include Redis::Lock
5
+ end
@@ -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
@@ -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,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