mongo-locking 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -1
- data/README.md +229 -0
- data/Rakefile +5 -1
- data/lib/mongo/locking/locker.rb +124 -97
- data/lib/mongo/locking/model_methods.rb +14 -18
- metadata +8 -8
- data/README.txt +0 -42
data/LICENSE
CHANGED
@@ -1 +1,20 @@
|
|
1
|
-
|
1
|
+
Copyright (c) 2011 Servio, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN THE EVENT THIS SOFTWARE GETS
|
16
|
+
YOU LAID OR MAKES YOU A MILLION DOLLARS, YOU AGREE HERETOFORE TO BUY JORDAN
|
17
|
+
RITTER A MONSTER TRUCK. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
19
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
### Summary
|
2
|
+
|
3
|
+
Mongo::Locking is a library that enables cross-process blocking mutexes, using
|
4
|
+
simple but flexible primitives to express an arbitrary graph of lock
|
5
|
+
dependencies between class instances.
|
6
|
+
|
7
|
+
#### Background
|
8
|
+
|
9
|
+
Consider the following common scenario:
|
10
|
+
|
11
|
+
Given an object graph 1 Order -> N OrderItems -> 1 JobFlow -> N Jobs, a
|
12
|
+
collection of disparate systems that operate on portions of the graph
|
13
|
+
asynchronously.
|
14
|
+
|
15
|
+
If you were using a Document-oriented (e.g. Mongo) data model, you might
|
16
|
+
represent the object graph as a bunch of nested objects, rooted in an Order. In
|
17
|
+
an RDBMS, you usually have a collection of tables with foreign key relationships
|
18
|
+
between them.
|
19
|
+
|
20
|
+
In any case, you need to enforce some notion of data integrity as portions of
|
21
|
+
the graph mutate. How does one normally enforce integrity in concurrent access
|
22
|
+
scenarios?
|
23
|
+
|
24
|
+
#### RDBMS
|
25
|
+
|
26
|
+
In the RDBMS world, you've got a couple options:
|
27
|
+
|
28
|
+
1. `SELECT .. FOR UPDATE` or equivalent.
|
29
|
+
|
30
|
+
Depending on the underlying storage engine, this will write lock at minimum
|
31
|
+
the given row, and in most modern RDBMS', a cluster of rows around the row
|
32
|
+
you're trying to "protect". This approach tends to require breaking out of
|
33
|
+
the ORM with custom SQL, and carries with it all sorts of
|
34
|
+
unintended/unexpected performance/synchronization/deadlock pitfalls. It
|
35
|
+
really starts to break down when there is more than one object that needs to
|
36
|
+
be "locked", multiple loosely-related objects that need to be "locked", or
|
37
|
+
when crossing database boundaries.
|
38
|
+
|
39
|
+
2. Rely on the ACID properties of SQL92 transactions to enforce data integrity.
|
40
|
+
|
41
|
+
(a) Given 2 or more competing, disparate processes running asynchronously,
|
42
|
+
accessing the same resources. Both enter into transactions, possibly access
|
43
|
+
overlapping resources, one wins and the other (eventually) fails after
|
44
|
+
attempting to commit.
|
45
|
+
|
46
|
+
Practically, what does the erroring code do? Does it retry? Was it written
|
47
|
+
in a way that even makes a retry possible? Is the context so consistently
|
48
|
+
atomic and stateless that it could blindly do so? Does it just bail and
|
49
|
+
fail? (Yes, most of the time.) What if it was acting on an asynchronous
|
50
|
+
imperative across a message bus? Shouldn't this condition be detected, and
|
51
|
+
the imperative replayed by some other code somewhere else? Wouldn't that
|
52
|
+
vary depending upon the logical atomicity of the operation? Etc etc.
|
53
|
+
|
54
|
+
(b) Given 2 competing processes, both enter into transactions, but the
|
55
|
+
relationship between resources is *not* fully expressed in terms of RDBMS
|
56
|
+
constraints. This is another very common case, as most ORMs tend to provide
|
57
|
+
integrity (validation) functionality in the app layer, and only a subset
|
58
|
+
actually trickle down into material RDBMS constraints. In this case, the
|
59
|
+
RDBMS has no idea that logical integrity has been violated, and general
|
60
|
+
misery will ensue.
|
61
|
+
|
62
|
+
Transactions are often mistakenly perceived as a panacea for these types of
|
63
|
+
of problems, and as a consequence usually compound the problems they are
|
64
|
+
being used to solve with additional complexity and cost.
|
65
|
+
|
66
|
+
### NoSQL
|
67
|
+
|
68
|
+
In the NoSQL world, you don't have as many options. Many folks mistakenly
|
69
|
+
believe that logically embedded objects are somehow protected by that nesting.
|
70
|
+
(They aren't.)
|
71
|
+
|
72
|
+
Some engines provide unique locking or pseudo-transactional primitive(s), but
|
73
|
+
generally the same costs and pitfalls of transactions apply. Especially so, in
|
74
|
+
distributed, partitioned environments.
|
75
|
+
|
76
|
+
### A Solution
|
77
|
+
|
78
|
+
However, when certain requirements are satisfied, one mechanism can
|
79
|
+
substantively bridge the gap: atomic increment/decrement. Anything that
|
80
|
+
implements it can be used to build a mutual-exclusion/locking system. And
|
81
|
+
that's what this library does.
|
82
|
+
|
83
|
+
Qualities:
|
84
|
+
|
85
|
+
- must be reasonably "fast" (hash-time lookup)
|
86
|
+
- must be non-blocking ("retry-able")
|
87
|
+
- must be recoverable (expiration of dead/stale locks)
|
88
|
+
- must be able to be monitored / administered
|
89
|
+
|
90
|
+
Behaviour:
|
91
|
+
|
92
|
+
- blocks for a configurable duration when acquiring a lock across execution threads
|
93
|
+
- *doesn't* block when (re-)acquiring a lock within the same thread of execution
|
94
|
+
|
95
|
+
|
96
|
+
.... TBC ....
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
### Usage
|
101
|
+
|
102
|
+
Mongo::Locking makes no effort to help configure the MongoDB connection - that's
|
103
|
+
what the Mongo Ruby Driver is for. However, when the collection is specified as
|
104
|
+
a proc, it will be lazily resolved during the first invocation of a lock. This
|
105
|
+
makes the concern of load/initialization order largely irrelevant.
|
106
|
+
|
107
|
+
Configuring Mongo::Locking with the Mongo Ruby Driver would look like this:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
::Mongo::Locking.configure(:collection => proc {
|
111
|
+
::Mongo::Connection.new("localhost").db("somedb").collection("locks")
|
112
|
+
})
|
113
|
+
```
|
114
|
+
|
115
|
+
Or using Mongoid:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
::Mongo::Locking.configure({
|
119
|
+
:collection => proc { ::Mongoid.database.collection("locks") },
|
120
|
+
:logger => Logger.new(STDOUT),
|
121
|
+
})
|
122
|
+
```
|
123
|
+
|
124
|
+
While Mongo::Locking depends on Mongo, it can be applied to any arbitrary object
|
125
|
+
model structure, including other ORMs. All locks have a namespace (scope) and a
|
126
|
+
key (some instance-related value), classes can depend on others for their locks,
|
127
|
+
and the dependency graph is resolved at invocation-time.
|
128
|
+
|
129
|
+
Consider this simplified example of using it with DataMapper:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
class Order
|
133
|
+
include ::DataMapper::Resource
|
134
|
+
include ::Mongo::Locking
|
135
|
+
|
136
|
+
has n, :order_items
|
137
|
+
|
138
|
+
lockable!
|
139
|
+
end
|
140
|
+
|
141
|
+
class OrderItem
|
142
|
+
include ::DataMapper::Resource
|
143
|
+
include ::Mongo::Locking
|
144
|
+
|
145
|
+
belongs_to :order
|
146
|
+
has 1, :job_flow
|
147
|
+
|
148
|
+
# invokes method to get "parent" lockable
|
149
|
+
locked_by! :order
|
150
|
+
end
|
151
|
+
|
152
|
+
class JobFlow
|
153
|
+
include ::DataMapper::Resource
|
154
|
+
include ::Mongo::Locking
|
155
|
+
|
156
|
+
belongs_to :order_item
|
157
|
+
|
158
|
+
# also takes a closure, yielding some abitrary "parent" lockable
|
159
|
+
locked_by! { |me| me.order_item }
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
Other (simplified) graph configuration imperatives:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
Order.lockable! :key => :id
|
167
|
+
Order.lockable! :scope => "OtherClass"
|
168
|
+
Order.lockable! :key => proc { |me| SHA1.hexdigest(me.balls) }
|
169
|
+
|
170
|
+
OrderItem.locked_by! { |me| me.order }
|
171
|
+
OrderItem.locked_by! :parent => proc { |me| me.order }
|
172
|
+
OrderItem.locked_by! :order
|
173
|
+
OrderItem.locked_by! :parent => :order
|
174
|
+
```
|
175
|
+
|
176
|
+
And then, a contrived use case:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
order = Order.get(1)
|
180
|
+
order.lock do
|
181
|
+
# ...
|
182
|
+
|
183
|
+
order.order_items.each do |item|
|
184
|
+
item.lock do
|
185
|
+
|
186
|
+
# this won't block even though the same lock is being acquired
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
Not blocking on lock re-acquisition means save hooks on models can be as
|
194
|
+
defensive as controller methods operating on them: both can lock, and it will
|
195
|
+
Just Work.
|
196
|
+
|
197
|
+
Pretty neat!
|
198
|
+
|
199
|
+
|
200
|
+
### Testing
|
201
|
+
|
202
|
+
Testing concurrency is "difficult", especially in Ruby. So for now, here's some
|
203
|
+
irb/console-level tests that you can use to test the library with:
|
204
|
+
|
205
|
+
Given:
|
206
|
+
|
207
|
+
Pn == process N
|
208
|
+
Order.id == 1
|
209
|
+
OrderItem.id == 1, OrderItem.order_id = 1
|
210
|
+
|
211
|
+
1. General race, same object
|
212
|
+
|
213
|
+
P1: Order.first.lock { debugger } # gets and holds lock
|
214
|
+
P2: Order.first.lock { puts "hi" } # retries acquire, fails
|
215
|
+
|
216
|
+
2. General race, locked root, attempt to lock from child
|
217
|
+
|
218
|
+
P1: Order.first.lock { debugger } # gets and holds lock
|
219
|
+
P2: OrderItem.first.lock { puts "hi" } # retries acquire, fails
|
220
|
+
|
221
|
+
3. General race, locked root from child, attempt to lock from child
|
222
|
+
|
223
|
+
P1: OrderItem.first.lock { debugger } # gets and holds lock
|
224
|
+
P2: OrderItem.first.lock { puts "hi" } # retries acquire, fails
|
225
|
+
|
226
|
+
4. Nested lock acquisition
|
227
|
+
|
228
|
+
P1: Order.first.lock { puts "1"; Order.first.lock { puts "2" } }
|
229
|
+
# should see 1 and 2
|
data/Rakefile
CHANGED
data/lib/mongo/locking/locker.rb
CHANGED
@@ -1,27 +1,28 @@
|
|
1
|
-
# Locker is a container for locking related commands, so we minimally impact the
|
2
|
-
# model's namespace.
|
3
|
-
#
|
4
|
-
# In addition to the Mongo-based, per-process, blocking lock mechanism itself,
|
5
|
-
# we also employ a thread-local lock refcount to achieve non-blocking behaviour
|
6
|
-
# when nesting lock closures. This is useful for when multiple, isolated code
|
7
|
-
# paths are all defensive with locks, but are arbitrarily called within the same
|
8
|
-
# thread of execution.
|
9
|
-
#
|
10
|
-
# We try to limit the number of these objects, so as to minimze extra cost
|
11
|
-
# attached to model instance hydration. Thus the Locker is attached to the
|
12
|
-
# model class, and maintains refcounts based on the key it's configured with.
|
13
|
-
|
14
1
|
require 'mongo/locking'
|
15
2
|
require 'active_support/core_ext/numeric/time'
|
16
|
-
require 'active_support/core_ext/object/blank'
|
17
3
|
|
18
4
|
module Mongo
|
19
5
|
module Locking
|
20
6
|
|
7
|
+
# Locker is a container for isolating all the locking-related methods,
|
8
|
+
# so we minimally impact the namespace of wherever we're mixed into.
|
9
|
+
#
|
10
|
+
# In addition to the Mongo-based, per-process, blocking lock mechanism
|
11
|
+
# itself, we also employ a thread-local lock refcount to achieve
|
12
|
+
# non-blocking behaviour when nesting lock closures. This is useful for
|
13
|
+
# when multiple, isolated code paths are all defensive with locks, but
|
14
|
+
# are arbitrarily called within the same thread of execution.
|
15
|
+
#
|
16
|
+
# We try to limit the number of these objects, so as to minimize extra
|
17
|
+
# cost attached to model instance hydration. Thus the Locker is
|
18
|
+
# attached to the model class, and maintains refcounts based on the key
|
19
|
+
# it's configured with.
|
20
|
+
|
21
21
|
class Locker
|
22
22
|
include Exceptions
|
23
23
|
|
24
24
|
attr_reader :config
|
25
|
+
delegate :debug, :info, :warn, :error, :fatal, :to => Locking
|
25
26
|
|
26
27
|
DEFAULT_OPTIONS = {
|
27
28
|
:max_retries => 5,
|
@@ -39,82 +40,92 @@ module Mongo
|
|
39
40
|
Thread.current[@refcount_key] ||= Hash.new(0)
|
40
41
|
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
key = locker.key_for(lockable)
|
47
|
-
scope = locker.scope_for(lockable)
|
48
|
-
|
49
|
-
locker.acquire(scope, key)
|
50
|
-
|
51
|
-
return lockable
|
52
|
-
end
|
53
|
-
|
54
|
-
def unlock(from)
|
43
|
+
# We increment refcounts ASARP so that any further (nested) calls
|
44
|
+
# won't block. But that means we have to make sure to decrement it
|
45
|
+
# on any failure case.
|
46
|
+
def acquire(from)
|
55
47
|
lockable = root_for(from)
|
56
48
|
locker = lockable.class.locker
|
57
|
-
|
58
|
-
key
|
59
|
-
scope
|
60
|
-
|
61
|
-
locker.release(scope, key)
|
62
|
-
|
63
|
-
return lockable
|
64
|
-
end
|
65
|
-
|
66
|
-
# refcounts[] is about enabling non-blocking behaviour when the
|
67
|
-
# process has already acquired the lock (e.g. controller locks
|
68
|
-
# Order, calls model save method that also locks Order). In a way,
|
69
|
-
# it's like an IdentityMap, mapping instance keys to reference
|
70
|
-
# counts (another reason it lives on the class and not the
|
71
|
-
# instance).
|
72
|
-
#
|
73
|
-
# We increment it immediately so that any further (nested) calls
|
74
|
-
# won't block, but that means we have to make sure to decrement it
|
75
|
-
# on every failure case.
|
76
|
-
def acquire(scope, key)
|
77
|
-
name = scope + "/" + key
|
49
|
+
scope = locker.scope_for(lockable)
|
50
|
+
key = locker.key_for(lockable)
|
51
|
+
name = scope + "/" + key
|
78
52
|
|
79
53
|
refcounts[key] += 1
|
80
54
|
if refcounts[key] > 1
|
81
|
-
|
82
|
-
return
|
55
|
+
info "acquire: re-using lock for #{name}##{refcounts[key]}"
|
56
|
+
return lockable
|
83
57
|
end
|
84
58
|
|
85
59
|
target = { :scope => scope, :key => key }
|
86
60
|
interval = self.config[:first_retry_interval]
|
87
61
|
retries = 0
|
88
62
|
|
89
|
-
|
63
|
+
debug "acquire: attempting lock of #{name}"
|
90
64
|
|
91
65
|
begin
|
92
66
|
a_lock = atomic_inc(target, { :refcount => 1 })
|
93
67
|
refcount = a_lock['refcount']
|
94
68
|
|
95
|
-
# FIXME: If the lock is "expired" then we just inherit the
|
96
|
-
# lock and assume the user of the lock is "gone", right? Do
|
97
|
-
# we need to do some decr/incr? Is this correct?
|
98
|
-
if a_lock['expire_at'] < Time.now
|
99
|
-
refcount = 1
|
100
|
-
end
|
101
|
-
|
102
69
|
# If the refcount is 0 or somehow less than 0, after we just
|
103
70
|
# incremented, then retry without counting against the max.
|
104
71
|
if refcount < 1
|
105
72
|
retries -= 1
|
106
|
-
|
73
|
+
debug "acquire: refcount #{refcount}, unexpected state"
|
107
74
|
raise LockFailure
|
108
75
|
end
|
109
76
|
|
77
|
+
# Check lock expiration.
|
78
|
+
if a_lock.has_key?('expire_at') and a_lock['expire_at'] < Time.now
|
79
|
+
|
80
|
+
# If the lock is "expired". We assume the owner of the
|
81
|
+
# lock is "gone" without decrementing the refcount.
|
82
|
+
warn "acquire: #{name} lock expired"
|
83
|
+
|
84
|
+
# Attempt to decrement the refcount to "reverse" the
|
85
|
+
# damage caused by the "gone" process. There might be
|
86
|
+
# more than one process trying to do this at the same
|
87
|
+
# time. Therefore, we need the refcount > 1 guard. If
|
88
|
+
# the lock's refcount is no longer > 1 by the time this
|
89
|
+
# process try to decrement, Mongo will raise a
|
90
|
+
# Mongo::OperationFailure. Regardless of reason, if it
|
91
|
+
# fails, we fail.
|
92
|
+
a_lock = atomic_inc(target.merge({:refcount => {'$gt' => 1} }), { :refcount => -1 }) rescue nil
|
93
|
+
|
94
|
+
unless a_lock
|
95
|
+
# We lost the race to "reverse" the damage. Someone
|
96
|
+
# else has the lock now. We will retry.
|
97
|
+
raise LockFailure
|
98
|
+
end
|
99
|
+
|
100
|
+
# We have won the race to "reverse" the damage but we
|
101
|
+
# may have not "reversed" enough of the damage.
|
102
|
+
# Consider the case that the expired lock has a large
|
103
|
+
# refcount - we still need to check refcount to make
|
104
|
+
# sure that we are have acquired the lock.
|
105
|
+
refcount = a_lock['refcount']
|
106
|
+
|
107
|
+
# The rest of the expired_lock handling logic coincides
|
108
|
+
# with normal lock logic.
|
109
|
+
end
|
110
|
+
|
110
111
|
# If recount is greater than 1, we lost the race. Decrement
|
111
112
|
# and try again.
|
112
113
|
if refcount > 1
|
113
114
|
atomic_inc(target, { :refcount => -1 })
|
114
|
-
|
115
|
+
debug "acquire: refcount #{refcount}, race lost"
|
115
116
|
raise LockFailure
|
116
117
|
end
|
117
118
|
|
119
|
+
# If refcount == 1, we have the lock and thus renew
|
120
|
+
# its expire_at.
|
121
|
+
#
|
122
|
+
# NOTE: This expire_at renewal only happens when a process
|
123
|
+
# acquires the lock for the first time. Subsequent lock
|
124
|
+
# reuse will NOT renew expire_at. This assumes that all
|
125
|
+
# legitimate operations should complete within
|
126
|
+
# config[:max_lifetime] time limit.
|
127
|
+
atomic_update(target, {'expire_at' => self.config[:max_lifetime].from_now})
|
128
|
+
|
118
129
|
rescue LockFailure => e
|
119
130
|
retries += 1
|
120
131
|
if retries >= self.config[:max_retries]
|
@@ -122,7 +133,7 @@ module Mongo
|
|
122
133
|
raise LockTimeout, "unable to acquire lock #{name}"
|
123
134
|
end
|
124
135
|
|
125
|
-
|
136
|
+
warn "acquire: #{name} refcount #{refcount}, retry #{retries} for lock"
|
126
137
|
|
127
138
|
sleep(interval.to_f)
|
128
139
|
interval = [self.config[:max_retry_interval].to_f, interval * 2].min
|
@@ -135,49 +146,57 @@ module Mongo
|
|
135
146
|
raise LockFailure, "unable to acquire lock #{name}"
|
136
147
|
end
|
137
148
|
|
138
|
-
|
149
|
+
info "acquire: #{name} locked (#{refcount})"
|
139
150
|
|
140
|
-
return
|
151
|
+
return lockable
|
141
152
|
end
|
142
153
|
|
143
|
-
def release(
|
144
|
-
|
154
|
+
def release(from)
|
155
|
+
lockable = root_for(from)
|
156
|
+
locker = lockable.class.locker
|
157
|
+
key = locker.key_for(lockable)
|
158
|
+
scope = locker.scope_for(lockable)
|
159
|
+
name = scope + "/" + key
|
145
160
|
|
146
161
|
refcounts[key] -= 1
|
147
162
|
if refcounts[key] > 0
|
148
|
-
|
163
|
+
info "release: re-using lock for #{name}##{refcounts[key]}"
|
149
164
|
return true
|
150
165
|
end
|
151
166
|
|
152
167
|
target = { :scope => scope, :key => key }
|
153
168
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
169
|
+
refcount = atomic_inc(target, { :refcount => -1 })['refcount']
|
170
|
+
|
171
|
+
info "release: #{name} unlocked (#{refcount})"
|
172
|
+
|
173
|
+
# If the refcount is at zero, nuke it out of the table.
|
174
|
+
#
|
175
|
+
# NOTE: If the following delete fails (e.g. something else
|
176
|
+
# incremented it before we tried to delete it), it will raise:
|
177
|
+
#
|
178
|
+
# <Mongo::OperationFailure: Database command 'findandmodify'
|
179
|
+
# failed: {"errmsg"=>"No matching object found", "ok"=>0.0}>
|
180
|
+
#
|
181
|
+
# This is normal for a concurrent system.
|
182
|
+
#
|
183
|
+
# Since a lock with refcount 0 does not impact lock functionality,
|
184
|
+
# we can also ignore any other exceptions during lock deletion.
|
185
|
+
#
|
186
|
+
# We use 'rescue nil' to ignore all exceptions.
|
187
|
+
if refcount == 0
|
188
|
+
if hash = atomic_delete(target.merge({ :refcount => 0 })) rescue nil
|
189
|
+
debug "release: lock #{name} no longer needed, deleted"
|
175
190
|
end
|
176
191
|
|
177
|
-
|
178
|
-
|
179
|
-
|
192
|
+
# Nuke the key from our instance refcounts so we don't
|
193
|
+
# balloon during long-lived processes.
|
194
|
+
refcounts.delete(key)
|
180
195
|
end
|
196
|
+
|
197
|
+
rescue => e
|
198
|
+
log_exception(e)
|
199
|
+
raise LockFailure, "unable to release lock #{name}"
|
181
200
|
end
|
182
201
|
|
183
202
|
def root_for(from)
|
@@ -218,7 +237,7 @@ module Mongo
|
|
218
237
|
|
219
238
|
def parent_for(lockable)
|
220
239
|
return case parent = self.config[:parent]
|
221
|
-
when Proc then parent.call(
|
240
|
+
when Proc then parent.call(lockable)
|
222
241
|
when Symbol then lockable.send(parent)
|
223
242
|
when NilClass then nil
|
224
243
|
else raise InvalidConfig, "unknown parent type #{parent.inspect}"
|
@@ -226,13 +245,14 @@ module Mongo
|
|
226
245
|
end
|
227
246
|
|
228
247
|
def is_root?
|
229
|
-
self.config[:parent].
|
248
|
+
self.config[:parent].nil?
|
230
249
|
end
|
231
250
|
|
232
251
|
private
|
233
252
|
|
253
|
+
# Separated out in case you want to monkeypatch it into oblivion.
|
234
254
|
def log_exception(e)
|
235
|
-
|
255
|
+
error e.inspect + " " + e.backtrace[0..4].inspect
|
236
256
|
end
|
237
257
|
|
238
258
|
# Notes:
|
@@ -255,10 +275,16 @@ module Mongo
|
|
255
275
|
:new => true, # Return the updated document
|
256
276
|
:upsert => true, # Update if document exists, insert if not
|
257
277
|
:query => search_hash.merge({'$atomic' => 1}),
|
258
|
-
:update => {
|
259
|
-
|
260
|
-
|
261
|
-
|
278
|
+
:update => {'$inc' => update_hash.dup},
|
279
|
+
})
|
280
|
+
end
|
281
|
+
|
282
|
+
def atomic_update(search_hash, update_hash)
|
283
|
+
return Locking.collection.find_and_modify({
|
284
|
+
:new => true, # Return the updated document
|
285
|
+
:upsert => true, # Update if document exists, insert if not
|
286
|
+
:query => search_hash.merge({'$atomic' => 1}),
|
287
|
+
:update => {'$set' => update_hash.dup},
|
262
288
|
})
|
263
289
|
end
|
264
290
|
|
@@ -272,3 +298,4 @@ module Mongo
|
|
272
298
|
end # Locker
|
273
299
|
end # Locking
|
274
300
|
end # Mongo
|
301
|
+
|
@@ -8,14 +8,6 @@ module Mongo
|
|
8
8
|
module ModelMethods
|
9
9
|
extend ::ActiveSupport::Concern
|
10
10
|
|
11
|
-
included do
|
12
|
-
# We don't want people modifying this attribute, but it needs to
|
13
|
-
# be accessible from the outside.
|
14
|
-
class << self
|
15
|
-
attr_reader :locker
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
11
|
module ClassMethods
|
20
12
|
|
21
13
|
# Options:
|
@@ -32,16 +24,13 @@ module Mongo
|
|
32
24
|
key = opts[:key] ||= :id
|
33
25
|
scope = opts[:scope] ||= self.name
|
34
26
|
|
35
|
-
raise ArgumentError, "locker already defined" if self.locker
|
36
27
|
raise ArgumentError, "locker key must be a Proc or Symbol" unless [Proc, Symbol].include?(key.class)
|
37
28
|
raise ArgumentError, "locker scope must be a Proc, Symbol or String" unless [Proc, Symbol, String].include?(scope.class)
|
38
29
|
|
39
30
|
opts[:class_name] = self.name
|
40
31
|
@locker = Locker.new(opts)
|
41
32
|
|
42
|
-
|
43
|
-
|
44
|
-
self
|
33
|
+
return self
|
45
34
|
end
|
46
35
|
|
47
36
|
# Options:
|
@@ -52,16 +41,18 @@ module Mongo
|
|
52
41
|
opts = args.extract_options!
|
53
42
|
parent = opts[:parent] ||= parent || args.first
|
54
43
|
|
55
|
-
raise ArgumentError, "locker already defined" if self.locker
|
56
44
|
raise ArgumentError, "parent reference must be a Proc or Symbol" unless [Proc, Symbol].include?(parent.class)
|
57
45
|
|
58
46
|
opts[:class_name] = self.name
|
59
47
|
|
60
48
|
@locker = Locker.new(opts)
|
61
49
|
|
62
|
-
|
50
|
+
return self
|
51
|
+
end
|
63
52
|
|
64
|
-
|
53
|
+
# No-frills class-inheritable locker reference
|
54
|
+
def locker
|
55
|
+
@locker ||= (superclass.locker if superclass.respond_to?(:locker))
|
65
56
|
end
|
66
57
|
|
67
58
|
end # ClassMethods
|
@@ -73,7 +64,11 @@ module Mongo
|
|
73
64
|
def lock(opts = {})
|
74
65
|
raise ArgumentError, "#{self.class.name}#lock requires a block" unless block_given?
|
75
66
|
|
76
|
-
|
67
|
+
# Look for the top level lockable
|
68
|
+
lockable = self.class.locker.root_for(self)
|
69
|
+
|
70
|
+
# Try to acquire the lock. If succeeds, "locked" will be set
|
71
|
+
locked = lockable.class.locker.acquire(lockable)
|
77
72
|
|
78
73
|
return yield
|
79
74
|
|
@@ -82,7 +77,7 @@ module Mongo
|
|
82
77
|
raise e
|
83
78
|
|
84
79
|
ensure
|
85
|
-
# Only unlock if
|
80
|
+
# Only unlock if "locked" was set. We're using this to
|
86
81
|
# distinguish between an exception from the yield vs. an
|
87
82
|
# exception from our own locking code. Doing it in an
|
88
83
|
# ensure block makes us defensible against a return from
|
@@ -91,7 +86,7 @@ module Mongo
|
|
91
86
|
# Calling lockable's locker instead of self potentially
|
92
87
|
# saves us the cost of "find root lockable" that locker
|
93
88
|
# would perform.
|
94
|
-
lockable.class.locker.
|
89
|
+
lockable.class.locker.release(lockable) if locked
|
95
90
|
end
|
96
91
|
|
97
92
|
end # InstanceMethods
|
@@ -99,3 +94,4 @@ module Mongo
|
|
99
94
|
end # ModelMethods
|
100
95
|
end # Locking
|
101
96
|
end # Mongo
|
97
|
+
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongo-locking
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jordan Ritter
|
@@ -17,7 +17,7 @@ autorequire:
|
|
17
17
|
bindir: bin
|
18
18
|
cert_chain: []
|
19
19
|
|
20
|
-
date: 2011-07-
|
20
|
+
date: 2011-07-22 00:00:00 -07:00
|
21
21
|
default_executable:
|
22
22
|
dependencies:
|
23
23
|
- !ruby/object:Gem::Dependency
|
@@ -52,18 +52,18 @@ dependencies:
|
|
52
52
|
version: 3.0.4
|
53
53
|
type: :runtime
|
54
54
|
version_requirements: *id002
|
55
|
-
description: A mixin DSL for implementing cross-process mutexes/locks using MongoDB
|
55
|
+
description: A mixin DSL for implementing cross-process mutexes/locks using MongoDB.
|
56
56
|
email: jpr5@serv.io
|
57
57
|
executables: []
|
58
58
|
|
59
59
|
extensions: []
|
60
60
|
|
61
61
|
extra_rdoc_files:
|
62
|
-
- README.
|
62
|
+
- README.md
|
63
63
|
- LICENSE
|
64
64
|
files:
|
65
65
|
- LICENSE
|
66
|
-
- README.
|
66
|
+
- README.md
|
67
67
|
- Rakefile
|
68
68
|
- lib/mongo/locking/locker.rb
|
69
69
|
- lib/mongo/locking/model_methods.rb
|
@@ -101,6 +101,6 @@ rubyforge_project:
|
|
101
101
|
rubygems_version: 1.6.2
|
102
102
|
signing_key:
|
103
103
|
specification_version: 3
|
104
|
-
summary: A mixin DSL for implementing cross-process mutexes/locks using MongoDB
|
104
|
+
summary: A mixin DSL for implementing cross-process mutexes/locks using MongoDB.
|
105
105
|
test_files: []
|
106
106
|
|
data/README.txt
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
##
|
2
|
-
## Usages
|
3
|
-
##
|
4
|
-
#
|
5
|
-
# Order.lockable!
|
6
|
-
# Order.lockable! :key => :id
|
7
|
-
# Order.lockable! :key => proc { |me| SHA1.hexdigest(me.balls) }
|
8
|
-
# Order.lockable! :scope => "OtherClass"
|
9
|
-
#
|
10
|
-
# OrderItem.locked_by! { |me| me.order }
|
11
|
-
# OrderItem.locked_by! :parent => proc { |me| me.order }
|
12
|
-
# OrderItem.locked_by! :order
|
13
|
-
# OrderItem.locked_by! :parent => :order
|
14
|
-
#
|
15
|
-
##
|
16
|
-
## Useful tests
|
17
|
-
##
|
18
|
-
#
|
19
|
-
# Pn == process N
|
20
|
-
# Order.id == 1
|
21
|
-
# OrderItem.id == 1, OrderItem.order_id = 1
|
22
|
-
#
|
23
|
-
# - General race, same object
|
24
|
-
#
|
25
|
-
# P1: Order.first.lock { debugger } # gets and holds lock
|
26
|
-
# P2: Order.first.lock { puts "hi" } # retries acquire, fails
|
27
|
-
#
|
28
|
-
# - General race, locked root, attempt to lock from child
|
29
|
-
#
|
30
|
-
# P1: Order.first.lock { debugger } # gets and holds lock
|
31
|
-
# P2: OrderItem.first.lock { puts "hi" } # retries acquire, fails
|
32
|
-
#
|
33
|
-
# - General race, locked root from child, attempt to lock from child
|
34
|
-
#
|
35
|
-
# P1: OrderItem.first.lock { debugger } # gets and holds lock
|
36
|
-
# P2: OrderItem.first.lock { puts "hi" } # retries acquire, fails
|
37
|
-
#
|
38
|
-
# - Nested lock acquisition
|
39
|
-
#
|
40
|
-
# P1: Order.first.lock { puts "1"; Order.first.lock { puts "2" } }
|
41
|
-
# # should see 1 and 2
|
42
|
-
#
|