mongo-locking 0.0.1 → 0.0.2
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/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
|
-
#
|