mlanett-redis-lock 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redis-lock.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem "guard-rspec"
8
+ gem "rb-fsevent" # for guard
9
+ gem "rspec"
10
+ gem "ruby-debug19", require: false
11
+ end
12
+
13
+ group :test do
14
+ gem "simplecov", require: false
15
+ end
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard "rspec" do
2
+ watch(%r{^lib/redis-lock\.rb$}) { "spec" }
3
+ watch(%r{^spec/.+_spec\.rb$})
4
+ watch("spec/helper.rb") { "spec" }
5
+ watch(%r{^spec/support/.+\.rb}) { "spec" }
6
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Mark Lanett
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Redis::Lock
2
+
3
+ This gem implements a pessimistic lock using Redis.
4
+ It correctly handles timeouts and vanishing lock owners (such as machine failures)
5
+
6
+ This uses setnx, but not the setnx algorithm described in the redis cookbook, which is not robust.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'redis-lock'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install redis-lock
21
+
22
+ ## Background
23
+
24
+ A lock needs an expected lifetime.
25
+ If the owner of a lock disappears (due to machine failure, network failure, process death),
26
+ you want the lock to expire and another owner to be able to acquire the lock.
27
+ At the same time, the owner of a lock should be able to extend its lifetime.
28
+ Thus, you can acquire a lock with a conservative estimate on lifetime, and extend it as necessary,
29
+ rather than acquiring the lock with a very long lifetime which will result in long waits in the event of failures.
30
+
31
+ A lock needs an owner. Redis::Lock defaults to using an owner id of HOSTNAME:PID.
32
+
33
+ A lock may need more than one attempt to acquire it. Redis::Lock offers a timeout; this defaults to 1 second.
34
+ It uses exponential backoff with sleeps so it's fairly safe to use longer timeouts.
35
+
36
+ ## Usage
37
+
38
+ This gem adds lock() and unlock() to Redis instances.
39
+ lock() takes a block and is safer than using lock() and unlock() separately.
40
+ lock() takes a key and lifetime and optionally a timeout (otherwise defaulting to 1 second).
41
+
42
+ redis.lock("test") { do_something }
43
+
44
+ ## Problems
45
+
46
+ Why do other gems get this wrong?
47
+
48
+ You need to be able to handle race conditions while acquiring the lock.
49
+ You need to be able to handle the owner of the lock failing to release it.
50
+ You need to be able to detect stale locks.
51
+ You need to handle race conditions while cleaning the stale lock and acquiring a new one.
52
+ The code which cleans the stale lock may not be able to assume it gets the new one.
53
+ The code which cleans the stale lock must not interfere with a different owner acquiring the lock.
54
+
55
+ ## Contributing
56
+
57
+ 1. Fork it
58
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
59
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
60
+ 4. Push to the branch (`git push origin my-new-feature`)
61
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:rspec)
6
+
7
+ task :default => [:rspec]
data/lib/redis-lock.rb ADDED
@@ -0,0 +1,225 @@
1
+ require "redis"
2
+ require "redis-lock/version"
3
+
4
+ class Redis
5
+
6
+ class Lock
7
+
8
+ class LockNotAcquired < StandardError
9
+ end
10
+
11
+ attr :redis
12
+ attr :key
13
+ attr :okey # key with redis namespace
14
+ attr :oval
15
+ attr :xkey # expiration key with redis namespace
16
+ attr :xval
17
+ attr :life, true # how long we expect to keep this lock locked
18
+ attr :logger, true
19
+
20
+ # @param redis is a Redis instance
21
+ # @param key is a unique string identifying the object to lock, e.g. "user-1"
22
+ # @param options[:life] may be set, but defaults to 1 minute
23
+ # @param options[:owner] may be set, but defaults to HOSTNAME:PID
24
+ def initialize( redis, key, options = {} )
25
+ check_keys( options, :owner, :life )
26
+ @redis = redis
27
+ @key = key
28
+ @okey = "lock:owner:#{key}"
29
+ @oval = options[:owner] || "#{`hostname`.strip}:#{Process.pid}"
30
+ @xkey = "lock:expire:#{key}"
31
+ @life = options[:life] || 60
32
+ end
33
+
34
+ def lock( timeout = 1, &block )
35
+ do_lock_with_timeout(timeout) or raise LockNotAcquired.new(key)
36
+ if block then
37
+ begin
38
+ block.call
39
+ ensure
40
+ release_lock
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ def extend_life( new_life )
47
+ do_extend( new_life ) or raise LockNotAcquired.new(key)
48
+ self
49
+ end
50
+
51
+ def unlock
52
+ release_lock
53
+ self
54
+ end
55
+
56
+ #
57
+ # queries
58
+ #
59
+
60
+ def locked?( now = Time.now.to_i )
61
+ # read both in a transaction in a multi to ensure we have a consistent view
62
+ result = redis.multi do |multi|
63
+ multi.get( okey )
64
+ multi.get( xkey )
65
+ end
66
+ result && result.size == 2 && is_locked?( result[0], result[1], now )
67
+ end
68
+
69
+ #
70
+ # internal api
71
+ #
72
+
73
+ def do_lock_with_timeout( timeout )
74
+ locked = false
75
+ with_timeout(timeout) { locked = do_lock }
76
+ locked
77
+ end
78
+
79
+ # @returns true if locked, false otherwise
80
+ def do_lock( tries = 2 )
81
+ # We need to set both owner and expire at the same time
82
+ # If the existing lock is stale, we delete it and try again once
83
+
84
+ loop do
85
+ new_xval = Time.now.to_i + life
86
+ result = redis.mapped_msetnx okey => oval, xkey => new_xval
87
+
88
+ if result == 1 then
89
+ log :debug, "do_lock() success"
90
+ @xval = new_xval
91
+ return true
92
+
93
+ else
94
+ log :debug, "do_lock() failed"
95
+ # consider the possibility that this lock is stale
96
+ tries -= 1
97
+ next if tries > 0 && stale_key?
98
+ return false
99
+ end
100
+ end
101
+ end
102
+
103
+ def do_extend( new_life, my_owner = oval )
104
+ # We use watch and a transaction to ensure we only change a lock we own
105
+ # The transaction fails if the watched variable changed
106
+ # Use my_owner = oval to make testing easier.
107
+ new_xval = Time.now.to_i + new_life
108
+ with_watch( okey ) do
109
+ owner = redis.get( okey )
110
+ if owner == my_owner then
111
+ result = redis.multi do |multi|
112
+ multi.set( xkey, new_xval )
113
+ end
114
+ if result && result.size == 1 then
115
+ log :debug, "do_extend() success"
116
+ @xval = new_xval
117
+ return true
118
+ end
119
+ end
120
+ end
121
+ return false
122
+ end
123
+
124
+ # Only actually deletes it if we own it.
125
+ # There may be strange cases where we fail to delete it, in which case expiration will solve the problem.
126
+ def release_lock( my_owner = oval )
127
+ # Use my_owner = oval to make testing easier.
128
+ with_watch( okey, xkey ) do
129
+ owner = redis.get( okey )
130
+ if owner == my_owner then
131
+ redis.multi do |multi|
132
+ multi.del( okey )
133
+ multi.del( xkey )
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def stale_key?( now = Time.now.to_i )
140
+ # Check if expiration exists and is it stale?
141
+ # If so, delete it.
142
+ # watch() both keys so we can detect if they change while we do this
143
+ # multi() will fail if keys have changed after watch()
144
+ # Thus, we snapshot consistency at the time of watch()
145
+ # Note: inside a watch() we get one and only one multi()
146
+ with_watch( okey, xkey ) do
147
+ owner = redis.get( okey )
148
+ expire = redis.get( xkey )
149
+ if is_deleteable?( owner, expire, now ) then
150
+ result = redis.multi do |r|
151
+ r.del( okey )
152
+ r.del( xkey )
153
+ end
154
+ # If anything changed then multi() fails and returns nil
155
+ if result && result.size == 2 then
156
+ log :info, "Deleted stale key from #{owner}"
157
+ return true
158
+ end
159
+ end
160
+ end # watch
161
+ # Not stale
162
+ return false
163
+ end
164
+
165
+ # Calls block until it returns true or times out. Uses exponential backoff.
166
+ # @param block should return true if successful, false otherwise
167
+ # @returns true if successful, false otherwise
168
+ def with_timeout( timeout, &block )
169
+ expire = Time.now + timeout.to_f
170
+ sleepy = 0.125
171
+ # this looks inelegant compared to while Time.now < expire, but does not oversleep
172
+ loop do
173
+ return true if block.call
174
+ log :debug, "Timeout" and return false if Time.now + sleepy > expire
175
+ sleep(sleepy)
176
+ sleepy *= 2
177
+ end
178
+ end
179
+
180
+ def with_watch( *args, &block )
181
+ # Note: watch() gets cleared by a multi() but it's safe to call unwatch() anyway.
182
+ redis.watch( *args )
183
+ begin
184
+ block.call
185
+ ensure
186
+ redis.unwatch
187
+ end
188
+ end
189
+
190
+ # @returns true if the lock exists and is owned by the given owner
191
+ def is_locked?( owner, expiration, now = Time.now.to_i )
192
+ owner == oval && ! is_deleteable?( owner, expiration, now )
193
+ end
194
+
195
+ # @returns true if this is a broken or expired lock
196
+ def is_deleteable?( owner, expiration, now = Time.now.to_i )
197
+ expiration = expiration.to_i
198
+ ( owner || expiration > 0 ) && ( ! owner || expiration < now )
199
+ end
200
+
201
+ def log( level, *messages )
202
+ if logger then
203
+ logger.send(level) { "[#{Time.now.strftime "%Y%m%d%H%M%S"} #{oval}] #{messages.join(' ')}" }
204
+ end
205
+ self
206
+ end
207
+
208
+ def check_keys( set, *keys )
209
+ extra = set.keys - keys
210
+ raise "Unknown Option #{extra.first}" if extra.size > 0
211
+ end
212
+
213
+ end # Lock
214
+
215
+ # Convenience methods
216
+
217
+ def lock( key, timeout = 1, options = {}, &block )
218
+ Lock.new( self, key, options ).lock( timeout, &block )
219
+ end
220
+
221
+ def unlock( key )
222
+ Lock( self, key ).unlock
223
+ end
224
+
225
+ end # Redis
@@ -0,0 +1,5 @@
1
+ class Redis
2
+ class Lock
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/redis-lock/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Mark Lanett"]
6
+ gem.email = ["mark.lanett@gmail.com"]
7
+ gem.description = %q{Pessimistic locking using Redis}
8
+ gem.summary = %q{Pessimistic locking using Redis}
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "mlanett-redis-lock"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Redis::Lock::VERSION
17
+
18
+ gem.add_dependency "redis"
19
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "bundler/setup" # set up gem paths
4
+ require "ruby-debug" # because sometimes you need it
5
+
6
+ require "simplecov" # code coverage
7
+ SimpleCov.start # must be loaded before our own code
8
+
9
+ require "redis-lock" # load this gem
10
+ require "support/redis" # simple helpers for testing
11
+
12
+ RSpec.configure do |spec|
13
+ # @see https://www.relishapp.com/rspec/rspec-core/docs/helper-methods/define-helper-methods-in-a-module
14
+ spec.include RedisClient, redis: true
15
+
16
+ # nuke the Redis database around each run
17
+ # @see https://www.relishapp.com/rspec/rspec-core/docs/hooks/around-hooks
18
+ spec.around( :each, redis: true ) do |example|
19
+ with_clean_redis { example.run }
20
+ end
21
+ end
@@ -0,0 +1,111 @@
1
+ require "helper"
2
+
3
+ describe Redis::Lock, redis: true do
4
+
5
+ let(:non) { nil }
6
+ let(:her) { "Alice" }
7
+ let(:him) { "Bob" }
8
+ let(:hers) { Redis::Lock.new( redis, "alpha", owner: her ) }
9
+ let(:her_same) { Redis::Lock.new( redis, "alpha", owner: her ) }
10
+ let(:his) { Redis::Lock.new( redis, "alpha", owner: him ) }
11
+ let(:his_other) { Redis::Lock.new( redis, "beta", owner: him ) }
12
+ let(:past ) { 1 }
13
+ let(:present) { 2 }
14
+ let(:future ) { 3 }
15
+
16
+ it "can acquire and release a lock" do
17
+ hers.lock do
18
+ hers.should be_locked
19
+ end
20
+ hers.should_not be_locked
21
+ end
22
+
23
+ it "can prevent other use of a lock" do
24
+ hers.lock do
25
+ expect { his.lock.unlock }.to raise_exception
26
+ end
27
+ expect { his.lock.unlock }.to_not raise_exception
28
+ end
29
+
30
+ it "can lock two different items at the same time" do
31
+ his.lock do
32
+ expect { his_other.lock.unlock }.to_not raise_exception
33
+ his.should be_locked
34
+ end
35
+ end
36
+
37
+ it "does not support nesting" do
38
+ hers.lock do
39
+ expect { her_same.lock }.to raise_exception
40
+ end
41
+ end
42
+
43
+ it "can acquire a lock" do
44
+ hers.do_lock.should be_true
45
+ end
46
+
47
+ it "can release a lock" do
48
+ hers.lock.release_lock
49
+ end
50
+
51
+ it "can use a timeout" do
52
+ hers.with_timeout(1) { true }.should be_true
53
+ hers.with_timeout(1) { false }.should be_false
54
+ # a few attempts are OK
55
+ results = [ false, false, true ]
56
+ hers.with_timeout(1) { results.shift }.should be_true
57
+ # this is too many attemps
58
+ results = [ false, false, false, false, false, true ]
59
+ hers.with_timeout(1) { results.shift }.should be_false
60
+ end
61
+
62
+ it "does not take too long to time out" do
63
+ start = Time.now.to_f
64
+ hers.with_timeout(1) { false }
65
+ time = Time.now.to_f - start
66
+ time.should be_within(0.2).of(1.0)
67
+ end
68
+
69
+ it "can time out an expired lock" do
70
+ hers.life = 1
71
+ hers.lock
72
+ # don't unlock it, let hers time out
73
+ expect { his.lock(10).unlock }.to_not raise_exception
74
+ end
75
+
76
+ it "can extend the life of a lock" do
77
+ hers.life = 1
78
+ hers.lock
79
+ hers.extend_life(100)
80
+ expect { his.lock(10).unlock }.to raise_exception
81
+ hers.unlock
82
+ end
83
+
84
+ it "can determine if it is locked" do
85
+ hers.is_locked?( non, nil, present ).should be_false
86
+ hers.is_locked?( non, future, present ).should be_false
87
+ hers.is_locked?( non, past, present ).should be_false
88
+ hers.is_locked?( her, nil, present ).should be_false
89
+ hers.is_locked?( her, future, present ).should be_true # the only valid case
90
+ hers.is_locked?( her, past, present ).should be_false
91
+ hers.is_locked?( him, nil, present ).should be_false
92
+ hers.is_locked?( him, future, present ).should be_false
93
+ hers.is_locked?( him, past, present ).should be_false
94
+ # We leave [ present, present ] to be unspecified.
95
+ end
96
+
97
+ it "can detect broken or expired locks" do
98
+ hers.is_deleteable?( non, nil, present ).should be_false # no lock => not expired
99
+
100
+ hers.is_deleteable?( non, future, present ).should be_true # broken => expired
101
+ hers.is_deleteable?( non, past, present ).should be_true # broken => expired
102
+ hers.is_deleteable?( her, nil, present ).should be_true # broken => expired
103
+
104
+ hers.is_deleteable?( her, future, present ).should be_false # current; not expired
105
+
106
+ hers.is_deleteable?( her, past, present ).should be_true # expired
107
+
108
+ # We leave [ present, present ] to be unspecified.
109
+ end
110
+
111
+ end
@@ -0,0 +1,46 @@
1
+ require "helper"
2
+
3
+ # These are here to be sure Redis works the way we expect.
4
+
5
+ describe Redis, redis: true do
6
+
7
+ it "can do a multi setnx" do
8
+ redis.mapped_msetnx "one" => "uno", "two" => "dos"
9
+ redis.get("one").should eq("uno")
10
+ redis.get("two").should eq("dos")
11
+ end
12
+
13
+ it "can delete multiple items" do
14
+ redis.set "one", "uno"
15
+ redis.set "two", "dos"
16
+ x = redis.multi do |multi|
17
+ multi.del "one"
18
+ multi.del "two"
19
+ end
20
+ x.should eq( [1,1] )
21
+ redis.get("one").should be_nil
22
+ redis.get("two").should be_nil
23
+ end
24
+
25
+ it "can detect multi success" do
26
+ redis.set "one", "uno"
27
+ with_watch( redis, "one" ) do
28
+ x = redis.multi do |multi|
29
+ multi.del "one"
30
+ end
31
+ x.should eq([1])
32
+ end
33
+ end
34
+
35
+ it "can detect multi failures" do
36
+ redis.set "one", "uno"
37
+ with_watch( redis, "one" ) do
38
+ x = redis.multi do |multi|
39
+ multi.del "one"
40
+ other.set "one", "ichi"
41
+ end
42
+ x.should be_nil
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "redis"
4
+
5
+ module RedisClient
6
+
7
+ TEST_REDIS = { url: "redis://127.0.0.1:6379/1" }
8
+
9
+ def redis
10
+ @redis ||= ::Redis.connect(TEST_REDIS)
11
+ end
12
+
13
+ def other
14
+ @other ||= ::Redis.connect(TEST_REDIS)
15
+ end
16
+
17
+ def with_watch( redis, *args )
18
+ redis.watch( *args )
19
+ begin
20
+ yield
21
+ ensure
22
+ redis.unwatch
23
+ end
24
+ end
25
+
26
+ def with_clean_redis(&block)
27
+ redis.client.disconnect # auto connect after fork
28
+ other.client.disconnect # auto connect after fork
29
+ redis.flushall # clean before run
30
+ yield
31
+ ensure
32
+ redis.flushall # clean up after run
33
+ redis.quit # quit (close) connection
34
+ other.quit # quit (close) connection
35
+ end
36
+
37
+ end # RedisClient
data/test/stress.rb ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup" # set up gem paths
4
+ require "redis-lock" # load this gem
5
+ require "optparse"
6
+ require "ostruct"
7
+
8
+ options = OpenStruct.new({
9
+ forks: 30,
10
+ tries: 10,
11
+ sleep: 2,
12
+ keys: 5
13
+ })
14
+
15
+ TEST_REDIS = { url: "redis://127.0.0.1:6379/1" }
16
+
17
+ OptionParser.new do |opts|
18
+ opts.banner = "Usage: #{__FILE__} --forks F --tries T --sleep S"
19
+ opts.on( "-f", "--forks FORKS", "How many processes to fork" ) { |i| options.forks = i.to_i }
20
+ opts.on( "-t", "--tries TRIES", "How many attempts each process should try" ) { |i| options.tries = i.to_i }
21
+ opts.on( "-s", "--sleep SLEEP", "How long processes should sleep/work" ) { |i| options.sleep = i.to_i }
22
+ opts.on( "-k", "--keys KEYS", "How many keys a process should run through" ) { |i| options.keys = i.to_i }
23
+ opts.on( "-h", "--help", "Display this usage summary" ) { puts opts; exit }
24
+ end.parse!
25
+
26
+ class Thing
27
+ attr :id
28
+ attr :activity
29
+ def initialize( id, activity )
30
+ @id = id
31
+ @activity = activity
32
+ end
33
+ def process
34
+ Kernel.sleep( rand activity )
35
+ end
36
+ end # Thing
37
+
38
+ class Runner
39
+
40
+ attr :options
41
+
42
+ def initialize( options )
43
+ @options = options
44
+ end
45
+
46
+ def redis
47
+ @redis ||= ::Redis.connect(TEST_REDIS)
48
+ end
49
+
50
+ def test( key, time )
51
+ redis.lock( key, time, life: time*2 ) do
52
+ val1 = rand(65536)
53
+ redis.set( "#{key}:widget", val1 )
54
+ Kernel.sleep( time )
55
+ val2 = redis.get("#{key}:widget").to_i
56
+ expect( val1, val2 )
57
+ end
58
+ true
59
+ rescue => x
60
+ # STDERR.puts "Failed due to #{x.inspect}"
61
+ false
62
+ end
63
+
64
+ def run
65
+ keys = Hash[ (0...options.keys).map { |i| [ i, "key:#{i}" ] } ] # i => key:i
66
+ fails = Hash[ (0...options.keys).map { |i| [ i, 0 ] } ] # i => 0
67
+ stats = OpenStruct.new( ok: 0, fails: 0 )
68
+ while keys.size > 0 do
69
+ i = keys.keys.sample
70
+ if test( keys[i], (options.sleep) ) then
71
+ keys.delete(i)
72
+ stats.ok += 1
73
+ else
74
+ fails[i] += 1
75
+ stats.fails += 1
76
+ if fails[i] >= options.tries then
77
+ keys.delete(i)
78
+ end
79
+ end
80
+ end
81
+ puts "[#{Process.pid}] Complete; Ok: #{stats.ok}, Failures: #{stats.fails}"
82
+ end
83
+
84
+ def launch
85
+ Kernel.fork do
86
+ GC.copy_on_write_friendly = true if ( GC.copy_on_write_friendly? rescue false )
87
+ run
88
+ end
89
+ end
90
+
91
+ def expect( val1, val2 )
92
+ if val1 != val2 then
93
+ STDERR.puts "[#{Process.pid}] Value mismatch"
94
+ Kernel.abort
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ # main
101
+
102
+ redis = ::Redis.connect(TEST_REDIS)
103
+ redis.flushall # clean before run
104
+ redis.client.disconnect # don't keep when forking
105
+
106
+ options.forks.times do
107
+ Runner.new( options ).launch
108
+ end
109
+ Process.waitall
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mlanett-redis-lock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mark Lanett
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-04 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &70233994826820 !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: *70233994826820
25
+ description: Pessimistic locking using Redis
26
+ email:
27
+ - mark.lanett@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - .rspec
34
+ - Gemfile
35
+ - Guardfile
36
+ - LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - lib/redis-lock.rb
40
+ - lib/redis-lock/version.rb
41
+ - redis-lock.gemspec
42
+ - spec/helper.rb
43
+ - spec/redis_lock_spec.rb
44
+ - spec/redis_spec.rb
45
+ - spec/support/redis.rb
46
+ - test/stress.rb
47
+ homepage: ''
48
+ licenses: []
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 1.8.15
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Pessimistic locking using Redis
71
+ test_files:
72
+ - spec/helper.rb
73
+ - spec/redis_lock_spec.rb
74
+ - spec/redis_spec.rb
75
+ - spec/support/redis.rb
76
+ - test/stress.rb