lock_key 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/.gitignore +17 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +62 -0
- data/Rakefile +8 -0
- data/lib/lock_key/lock_key.rb +156 -0
- data/lib/lock_key/version.rb +3 -0
- data/lib/lock_key.rb +7 -0
- data/lock_key.gemspec +19 -0
- data/spec/lock_key_spec.rb +51 -0
- data/spec/spec_helper.rb +4 -0
- metadata +58 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 TODO: Write your name
|
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,62 @@
|
|
1
|
+
# LockKey
|
2
|
+
|
3
|
+
Provides basic locking in Redis
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'lock_key'
|
10
|
+
|
11
|
+
If running on 1.9 that's it, if not, you'll need to also install uuid
|
12
|
+
|
13
|
+
gem 'uuid'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install lock_key
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Based on [redis-lock](https://github.com/PatrickTulskie/redis-lock) and the [setnx redis comments](http://redis.io/commands/setnx)
|
26
|
+
LockKey provides basic key leve locking.
|
27
|
+
|
28
|
+
## Locking a key
|
29
|
+
|
30
|
+
r = Redis.new
|
31
|
+
|
32
|
+
# use the Redis::LockKey.defaults
|
33
|
+
r.lock_key "some_key" do
|
34
|
+
# stuff in here with the key locked
|
35
|
+
end
|
36
|
+
|
37
|
+
# Selectively overwrite the defaults
|
38
|
+
r.lock_key "some_key", :expire => 3 do
|
39
|
+
# stuff in here with the key locked
|
40
|
+
end
|
41
|
+
|
42
|
+
Using the block version of lock\_key ensures that the lock is removed at the end of the block
|
43
|
+
|
44
|
+
If you need more control over the locking and unlocking do not use a block. Just be sure to ensure you remove the lock.
|
45
|
+
|
46
|
+
r.lock_key "some_key"
|
47
|
+
# do stuff here
|
48
|
+
r.unlock_key "some_key"
|
49
|
+
|
50
|
+
If worst comes to worst, you can forcefully kill the lock
|
51
|
+
|
52
|
+
r.kill_lock! "some_key"
|
53
|
+
|
54
|
+
NOTE: You should always minimise the size of the lock. Do your best not to wrap external calls in a lock
|
55
|
+
|
56
|
+
## Contributing
|
57
|
+
|
58
|
+
1. Fork it
|
59
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
60
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
61
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
62
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
class Redis
|
2
|
+
module LockKey
|
3
|
+
begin
|
4
|
+
require 'securerandom'
|
5
|
+
UUID_GEN = lambda { SecureRandom.uuid }
|
6
|
+
rescue LoadError
|
7
|
+
begin
|
8
|
+
require 'uuid'
|
9
|
+
UUID_GEN = lambda { UUID.new.generate }
|
10
|
+
rescue LoadError
|
11
|
+
puts <<-TXT
|
12
|
+
Could not find a uuid generator.
|
13
|
+
Ensure ActiveSupport is available for SecureRandom
|
14
|
+
OR
|
15
|
+
Install uuid gem
|
16
|
+
We prefer SecureRandom
|
17
|
+
TXT
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class LockAttemptTimeout < StandardError; end
|
22
|
+
|
23
|
+
@@defaults = {
|
24
|
+
:wait_for => 60, # seconds to wait to obtain a lock
|
25
|
+
:expire => 60, # seconds till key expires
|
26
|
+
:raise => true, # raise a LockKey::LockAttemptTimeout if the lock cannot be obtained
|
27
|
+
:sleep_for => 0.5
|
28
|
+
}
|
29
|
+
|
30
|
+
@@value_delimeter = "-:-:-"
|
31
|
+
|
32
|
+
def self.value_delimeter; @@value_delimeter; end
|
33
|
+
def self.value_delimeter=(del); @@value_delimeter = del; end
|
34
|
+
|
35
|
+
def self.defaults=(defaults); @@defaults = @@defaults.merge(defaults); end
|
36
|
+
def self.defaults; @@defaults; end
|
37
|
+
|
38
|
+
# The lock key id for this thread. Uses uuid so that concurrency is not an issue
|
39
|
+
# w.r.t. keys
|
40
|
+
def self.lock_key_id; Thread.current[:lock_key_id] ||= UUID_GEN.call; end
|
41
|
+
|
42
|
+
# Locks a key in redis options are same as default.
|
43
|
+
# If a block is given the lock is automatically released
|
44
|
+
# If no block is given, be sure to unlock the key when you're done.
|
45
|
+
# Note... Locks should be as _Small_ as possible with respec to the time you
|
46
|
+
# have the lock for!
|
47
|
+
# @param key String The key to lock
|
48
|
+
# @param opts Hash the options hash for the lock
|
49
|
+
# @option opts :wait_for Numeric The time to wait for to obtain a lock
|
50
|
+
# @option opts :expire Numeric The time before the lock expires
|
51
|
+
# @option opts :raise Causes a raise if a lock cannot be obtained
|
52
|
+
# @option opts :sleep_for the time to sleep between checks
|
53
|
+
def lock_key(key, opts={})
|
54
|
+
is_block, got_lock = block_given?, false
|
55
|
+
options = LockKey.defaults.merge(opts)
|
56
|
+
|
57
|
+
got_lock = obtain_lock(key, options)
|
58
|
+
yield if is_block && got_lock
|
59
|
+
got_lock
|
60
|
+
ensure
|
61
|
+
unlock_key(key, options) if is_block && got_lock
|
62
|
+
end
|
63
|
+
|
64
|
+
def locked_key?(key)
|
65
|
+
!lock_expired?(_redis_.get(lock_key_for(key)))
|
66
|
+
end
|
67
|
+
|
68
|
+
def kill_lock!(key)
|
69
|
+
_redis_.del(lock_key_for(key))
|
70
|
+
end
|
71
|
+
|
72
|
+
# Unlocks the key. Use a block... then you don't need this
|
73
|
+
# @param key String the key to unlock
|
74
|
+
# @param opts Hash an options hash
|
75
|
+
# @option opts :key the value of the key to unlock.
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# # Unlock the key if this thread owns it.
|
79
|
+
# redis.lock_key "foo"
|
80
|
+
# # do stuff
|
81
|
+
# redis.unlock_key "foo"
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# # Unlock the key in a multithreaded env
|
85
|
+
# key_value = redis.lock_key "foo"
|
86
|
+
# Thread.new do
|
87
|
+
# # do stuff
|
88
|
+
# redis.unlock_key "foo", :key => key_value
|
89
|
+
# end
|
90
|
+
def unlock_key(key, opts={})
|
91
|
+
lock_key = opts[:key]
|
92
|
+
value = _redis_.get(lock_key_for(key))
|
93
|
+
return true unless value
|
94
|
+
if value == lock_key || i_have_the_lock?(value)
|
95
|
+
kill_lock!(key)
|
96
|
+
true
|
97
|
+
else
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def _redis_
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def lock_key_for(key)
|
108
|
+
"lock_key:#{key}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def lock_value_for(key, opts)
|
112
|
+
"#{(Time.now + opts[:expire]).to_i}#{value_delimeter}#{LockKey.lock_key_id}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def value_delimeter
|
116
|
+
LockKey.value_delimeter
|
117
|
+
end
|
118
|
+
|
119
|
+
def obtain_lock(key, opts={})
|
120
|
+
_key_ = lock_key_for(key)
|
121
|
+
_value_ = lock_value_for(key,opts)
|
122
|
+
return _value_ if _redis_.setnx(_key_, _value_)
|
123
|
+
|
124
|
+
got_lock = false
|
125
|
+
wait_until = Time.now + opts[:wait_for]
|
126
|
+
|
127
|
+
until got_lock || Time.now > wait_until
|
128
|
+
current_lock = _redis_.get(_key_)
|
129
|
+
if lock_expired?(current_lock)
|
130
|
+
_value_ = lock_value_for(key,opts)
|
131
|
+
new_lock = _redis_.getset(_key_, _value_)
|
132
|
+
got_lock = new_lock if i_have_the_lock?(new_lock)
|
133
|
+
end
|
134
|
+
sleep opts[:sleep_for]
|
135
|
+
end
|
136
|
+
|
137
|
+
if !got_lock && opts[:raise]
|
138
|
+
_value_ = lock_value_for(key, opts)
|
139
|
+
raise LockAttemptTimeout, "Could not lock #{_value_}"
|
140
|
+
end
|
141
|
+
|
142
|
+
got_lock
|
143
|
+
end
|
144
|
+
|
145
|
+
def lock_expired?(lock_value)
|
146
|
+
return true if lock_value.nil?
|
147
|
+
exp = lock_value.split(value_delimeter).first
|
148
|
+
Time.now.to_i > exp.to_i
|
149
|
+
end
|
150
|
+
|
151
|
+
def i_have_the_lock?(lock_value)
|
152
|
+
return false unless lock_value
|
153
|
+
lock_value.split(value_delimeter).last == LockKey.lock_key_id
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/lock_key.rb
ADDED
data/lock_key.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lock_key/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "lock_key"
|
8
|
+
gem.version = LockKey::VERSION
|
9
|
+
gem.authors = ["Take out locks via redis"]
|
10
|
+
gem.email = ["has.sox@gmail.com"]
|
11
|
+
gem.description = %q{Uses redis to take out multi-threaded/processed safe locks}
|
12
|
+
gem.summary = %q{Uses redis to take out multi-threaded/processed safe locks}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "LockKey" do
|
4
|
+
before do
|
5
|
+
REDIS.flushdb
|
6
|
+
end
|
7
|
+
|
8
|
+
after do
|
9
|
+
REDIS.unlock_key "foo"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "takes out a lock" do
|
13
|
+
REDIS.lock_key "foo"
|
14
|
+
REDIS.locked_key?("foo").should be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
it "removes a lock" do
|
18
|
+
REDIS.lock_key "foo" do
|
19
|
+
REDIS.locked_key?("foo").should be_true
|
20
|
+
end
|
21
|
+
REDIS.locked_key?("foo").should be_false
|
22
|
+
end
|
23
|
+
|
24
|
+
it "removes a lock manually" do
|
25
|
+
REDIS.lock_key "foo"
|
26
|
+
REDIS.locked_key?("foo").should be_true
|
27
|
+
REDIS.unlock_key "foo"
|
28
|
+
REDIS.locked_key?("foo").should be_false
|
29
|
+
end
|
30
|
+
|
31
|
+
it "handles many threads" do
|
32
|
+
captures = []
|
33
|
+
one = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 2; captures << :one } }
|
34
|
+
two = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 1; captures << :two } }
|
35
|
+
three = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 2; captures << :three } }
|
36
|
+
four = lambda{ REDIS.lock_key("foo", :expire => 1, wait_for: 1) { sleep 2; captures << :four } }
|
37
|
+
|
38
|
+
threads = []
|
39
|
+
|
40
|
+
threads << Thread.new(&one)
|
41
|
+
threads << Thread.new(&two)
|
42
|
+
threads << Thread.new(&three)
|
43
|
+
threads << Thread.new(&four)
|
44
|
+
|
45
|
+
threads.each { |t| t.join }
|
46
|
+
|
47
|
+
captures.should have(3).elements
|
48
|
+
captures.should_not include(:four)
|
49
|
+
captures.should include(:one, :two, :three)
|
50
|
+
end
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lock_key
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Take out locks via redis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-06 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Uses redis to take out multi-threaded/processed safe locks
|
15
|
+
email:
|
16
|
+
- has.sox@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- lib/lock_key.rb
|
27
|
+
- lib/lock_key/lock_key.rb
|
28
|
+
- lib/lock_key/version.rb
|
29
|
+
- lock_key.gemspec
|
30
|
+
- spec/lock_key_spec.rb
|
31
|
+
- spec/spec_helper.rb
|
32
|
+
homepage: ''
|
33
|
+
licenses: []
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
requirements: []
|
51
|
+
rubyforge_project:
|
52
|
+
rubygems_version: 1.8.24
|
53
|
+
signing_key:
|
54
|
+
specification_version: 3
|
55
|
+
summary: Uses redis to take out multi-threaded/processed safe locks
|
56
|
+
test_files:
|
57
|
+
- spec/lock_key_spec.rb
|
58
|
+
- spec/spec_helper.rb
|