mlanett-redis-lock 0.0.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/.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