lock_key 0.1.1 → 0.2.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/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: