lock_key 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +1 -1
- data/lib/lock_key/lock_key.rb +56 -44
- data/lib/lock_key/version.rb +1 -1
- data/spec/lock_key_spec.rb +17 -5
- data/spec/spec_helper.rb +0 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -23,7 +23,7 @@ Or install it yourself as:
|
|
23
23
|
## Usage
|
24
24
|
|
25
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
|
26
|
+
LockKey provides basic key level locking.
|
27
27
|
|
28
28
|
## Locking a key
|
29
29
|
|
data/lib/lock_key/lock_key.rb
CHANGED
@@ -27,25 +27,25 @@ class Redis
|
|
27
27
|
:sleep_for => 0.5
|
28
28
|
}
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
def self.value_delimeter; @@value_delimeter; end
|
33
|
-
def self.value_delimeter=(del); @@value_delimeter = del; end
|
30
|
+
VALUE_DELIMETER = "-:-:-"
|
34
31
|
|
35
32
|
def self.defaults=(defaults); @@defaults = @@defaults.merge(defaults); end
|
36
33
|
def self.defaults; @@defaults; end
|
37
34
|
|
38
35
|
# The lock key id for this thread. Uses uuid so that concurrency is not an issue
|
39
|
-
#
|
36
|
+
# with respect to keys
|
40
37
|
def self.lock_key_id; Thread.current[:lock_key_id] ||= UUID_GEN.call; end
|
41
38
|
|
42
39
|
# Locks a key in redis options are same as default.
|
43
40
|
# If a block is given the lock is automatically released
|
44
41
|
# If no block is given, be sure to unlock the key when you're done.
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
42
|
+
#
|
43
|
+
# Note: Concurrency is hard, and deadlock is always a danger. Don't do any
|
44
|
+
# more than is absolutely needed inside the lock so that the lock exists
|
45
|
+
# for the shortest time possible.
|
46
|
+
#
|
47
|
+
# @param key String The Redis key to lock
|
48
|
+
# @param opts Hash The options hash for the lock
|
49
49
|
# @option opts :wait_for Numeric The time to wait for to obtain a lock
|
50
50
|
# @option opts :expire Numeric The time before the lock expires
|
51
51
|
# @option opts :raise Causes a raise if a lock cannot be obtained
|
@@ -62,17 +62,23 @@ class Redis
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def locked_key?(key)
|
65
|
-
|
65
|
+
!!fetch(key)
|
66
66
|
end
|
67
67
|
|
68
68
|
def kill_lock!(key)
|
69
69
|
_redis_.del(lock_key_for(key))
|
70
70
|
end
|
71
71
|
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
72
|
+
# Manually unlocks a key.
|
73
|
+
#
|
74
|
+
# This method is mainly intended for applications where the lock is obtained
|
75
|
+
# in one thread and then passed to another thread to be released. If you
|
76
|
+
# are obtaining and releasing a lock in the same thread, you should prefer
|
77
|
+
# the block form of lock_key over this method.
|
78
|
+
#
|
79
|
+
# @param key String The Redis key to unlock
|
80
|
+
# @param opts Hash An options hash
|
81
|
+
# @option opts :key The value of the key to unlock.
|
76
82
|
#
|
77
83
|
# @example
|
78
84
|
# # Unlock the key if this thread owns it.
|
@@ -88,10 +94,10 @@ class Redis
|
|
88
94
|
# redis.unlock_key "foo", :key => key_value
|
89
95
|
# end
|
90
96
|
def unlock_key(key, opts={})
|
91
|
-
|
92
|
-
value =
|
97
|
+
held_value = opts[:key]
|
98
|
+
value = fetch(key)
|
93
99
|
return true unless value
|
94
|
-
if value ==
|
100
|
+
if value == held_value || i_have_the_lock?(value)
|
95
101
|
kill_lock!(key)
|
96
102
|
true
|
97
103
|
else
|
@@ -104,54 +110,60 @@ class Redis
|
|
104
110
|
self
|
105
111
|
end
|
106
112
|
|
107
|
-
def
|
108
|
-
|
113
|
+
def fetch(key)
|
114
|
+
_redis_.get lock_key_for(key)
|
109
115
|
end
|
110
116
|
|
111
|
-
def
|
112
|
-
"
|
117
|
+
def lock_key_for(key)
|
118
|
+
"lock_key:#{key}"
|
113
119
|
end
|
114
120
|
|
115
|
-
def
|
116
|
-
LockKey.
|
121
|
+
def lock_key_value
|
122
|
+
LockKey.lock_key_id
|
117
123
|
end
|
118
124
|
|
119
125
|
def obtain_lock(key, opts={})
|
120
|
-
|
121
|
-
|
122
|
-
return _value_ if _redis_.setnx(_key_, _value_)
|
126
|
+
new_lock = renew_lock_if_owned(key,opts)
|
127
|
+
return new_lock if new_lock
|
123
128
|
|
124
|
-
got_lock = false
|
125
129
|
wait_until = Time.now + opts[:wait_for]
|
126
130
|
|
127
|
-
until
|
128
|
-
|
129
|
-
if
|
130
|
-
|
131
|
-
|
132
|
-
got_lock = new_lock if i_have_the_lock?(new_lock)
|
133
|
-
elsif i_have_the_lock?(current_lock)
|
134
|
-
got_lock = current_lock
|
131
|
+
until new_lock || Time.now > wait_until
|
132
|
+
_key_ = lock_key_for(key)
|
133
|
+
if _redis_.setnx(_key_, lock_key_value)
|
134
|
+
new_lock = lock_key_value
|
135
|
+
_redis_.expire(_key_, opts[:expire])
|
135
136
|
end
|
136
137
|
sleep opts[:sleep_for]
|
137
138
|
end
|
138
139
|
|
139
|
-
if !
|
140
|
+
if !new_lock && opts[:raise]
|
140
141
|
raise LockAttemptTimeout, "Could not lock #{key}"
|
141
142
|
end
|
142
143
|
|
143
|
-
|
144
|
-
end
|
145
|
-
|
146
|
-
def lock_expired?(lock_value)
|
147
|
-
return true if lock_value.nil?
|
148
|
-
exp = lock_value.split(value_delimeter).first
|
149
|
-
Time.now.to_i > exp.to_i
|
144
|
+
new_lock
|
150
145
|
end
|
151
146
|
|
152
147
|
def i_have_the_lock?(lock_value)
|
153
148
|
return false unless lock_value
|
154
|
-
|
149
|
+
# should just be a straight comparison, but need to roll out
|
150
|
+
# new code that changes things first
|
151
|
+
lock_value.split(VALUE_DELIMETER).last == LockKey.lock_key_id
|
152
|
+
end
|
153
|
+
|
154
|
+
def i_dont_have_the_lock?(lock_value)
|
155
|
+
!i_have_the_lock?(lock_value)
|
156
|
+
end
|
157
|
+
|
158
|
+
def renew_lock_if_owned(key,opts)
|
159
|
+
current_value = fetch(key)
|
160
|
+
return false if current_value && i_dont_have_the_lock?(current_value)
|
161
|
+
_key_ = lock_key_for(key)
|
162
|
+
result = _redis_.multi do
|
163
|
+
_redis_.set(_key_, lock_key_value)
|
164
|
+
_redis_.expire(_key_, opts[:expire])
|
165
|
+
end
|
166
|
+
lock_key_value if result.first == "OK"
|
155
167
|
end
|
156
168
|
end
|
157
169
|
end
|
data/lib/lock_key/version.rb
CHANGED
data/spec/lock_key_spec.rb
CHANGED
@@ -5,10 +5,6 @@ describe "LockKey" do
|
|
5
5
|
REDIS.flushdb
|
6
6
|
end
|
7
7
|
|
8
|
-
after do
|
9
|
-
REDIS.unlock_key "foo"
|
10
|
-
end
|
11
|
-
|
12
8
|
it "takes out a lock" do
|
13
9
|
REDIS.lock_key "foo"
|
14
10
|
REDIS.locked_key?("foo").should be_true
|
@@ -28,12 +24,28 @@ describe "LockKey" do
|
|
28
24
|
REDIS.locked_key?("foo").should be_false
|
29
25
|
end
|
30
26
|
|
27
|
+
it "lets you request the same lock many times" do
|
28
|
+
REDIS.lock_key "foo"
|
29
|
+
expect { REDIS.lock_key "foo" }.to_not raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it "raises an error when it can't get a lock" do
|
33
|
+
REDIS.lock_key("foo", :expire => 10)
|
34
|
+
t = Thread.new do
|
35
|
+
expect do
|
36
|
+
REDIS.lock_key("foo", :wait_for => 1) { sleep 1 }
|
37
|
+
end.to raise_error
|
38
|
+
end
|
39
|
+
|
40
|
+
t.join
|
41
|
+
end
|
42
|
+
|
31
43
|
it "handles many threads" do
|
32
44
|
captures = []
|
33
45
|
one = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 2; captures << :one } }
|
34
46
|
two = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 1; captures << :two } }
|
35
47
|
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 } }
|
48
|
+
four = lambda{ REDIS.lock_key("foo", :expire => 1, wait_for: 1, raise: false) { sleep 2; captures << :four } }
|
37
49
|
|
38
50
|
threads = []
|
39
51
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lock_key
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-09 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Uses redis to take out multi-threaded/processed safe locks
|
15
15
|
email:
|