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 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 leve locking.
26
+ LockKey provides basic key level locking.
27
27
 
28
28
  ## Locking a key
29
29
 
@@ -27,25 +27,25 @@ class Redis
27
27
  :sleep_for => 0.5
28
28
  }
29
29
 
30
- @@value_delimeter = "-:-:-"
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
- # w.r.t. keys
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
- # 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
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
- !lock_expired?(_redis_.get(lock_key_for(key)))
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
- # 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.
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
- lock_key = opts[:key]
92
- value = _redis_.get(lock_key_for(key))
97
+ held_value = opts[:key]
98
+ value = fetch(key)
93
99
  return true unless value
94
- if value == lock_key || i_have_the_lock?(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 lock_key_for(key)
108
- "lock_key:#{key}"
113
+ def fetch(key)
114
+ _redis_.get lock_key_for(key)
109
115
  end
110
116
 
111
- def lock_value_for(key, opts)
112
- "#{(Time.now + opts[:expire]).to_i}#{value_delimeter}#{LockKey.lock_key_id}"
117
+ def lock_key_for(key)
118
+ "lock_key:#{key}"
113
119
  end
114
120
 
115
- def value_delimeter
116
- LockKey.value_delimeter
121
+ def lock_key_value
122
+ LockKey.lock_key_id
117
123
  end
118
124
 
119
125
  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_)
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 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
- 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 !got_lock && opts[:raise]
140
+ if !new_lock && opts[:raise]
140
141
  raise LockAttemptTimeout, "Could not lock #{key}"
141
142
  end
142
143
 
143
- got_lock
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
- lock_value.split(value_delimeter).last == LockKey.lock_key_id
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
@@ -1,3 +1,3 @@
1
1
  module LockKey
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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
@@ -1,4 +1,3 @@
1
1
  require 'lock_key'
2
2
 
3
3
  REDIS = Redis.new
4
- puts REDIS.inspect
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.1.1
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-06 00:00:00.000000000 Z
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: