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 CHANGED
@@ -1 +1,20 @@
1
- Still working this out.
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.
@@ -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
@@ -1,3 +1,7 @@
1
1
  task :default do
2
- puts "TBC"
2
+ Kernel.exec("#{$0}", '-T')
3
+ end
4
+
5
+ task :gem do
6
+ `gem build mongo-locking.gemspec`
3
7
  end
@@ -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
- def lock(from)
43
- lockable = root_for(from)
44
- locker = lockable.class.locker
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 = locker.key_for(lockable)
59
- scope = locker.scope_for(lockable)
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
- Locking.info "acquire: re-using lock for #{name}##{refcounts[key]}"
82
- return true
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
- Locking.debug "acquire: attempting lock of #{name}"
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
- Locking.debug "acquire: refcount #{refcount}, unexpected state"
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
- Locking.debug "acquire: refcount #{refcount}, race lost"
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
- Locking.warn "acquire: #{name} refcount #{refcount}, retry #{retries} for lock"
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
- Locking.info "acquire: #{name} locked (#{refcount})"
149
+ info "acquire: #{name} locked (#{refcount})"
139
150
 
140
- return true
151
+ return lockable
141
152
  end
142
153
 
143
- def release(scope, key)
144
- name = scope + "/" + key
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
- Locking.info "release: re-using lock for #{name}##{refcounts[key]}"
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
- begin
155
- refcount = atomic_inc(target, { :refcount => -1 })['refcount']
156
-
157
- Locking.info "release: #{name} unlocked (#{refcount})"
158
-
159
- # If the refcount is at zero, nuke it out of the table.
160
- #
161
- # FIXME: If the following delete fails (e.g. something else
162
- # incremented it before we tried to delete it), it will
163
- # raise:
164
- #
165
- # <Mongo::OperationFailure: Database command 'findandmodify'
166
- # failed: {"errmsg"=>"No matching object found", "ok"=>0.0}>
167
- #
168
- # Need to see if there's a way to report the error
169
- # informationally instead of exceptionally. 'rescue nil'
170
- # hack in place until something more correct is put in.
171
- if refcount == 0
172
- unless hash = atomic_delete(target.merge({ :refcount => 0 })) rescue nil
173
- Locking.debug "release: lock #{name} no longer needed, deleted"
174
- end
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
- rescue => e
178
- log_exception(e)
179
- raise LockFailure, "unable to release lock #{name}"
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(self)
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].blank?
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
- Locking.error e.inspect + " " + e.backtrace[0..4].inspect
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
- '$inc' => update_hash.dup,
260
- '$set' => { 'expire_at' => self.config[:max_lifetime].from_now }
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
- Locking.debug "#{self.name} is lockable (#{opts.inspect})"
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
- Locking.debug "#{self.name} is locked_by (#{opts.inspect})"
50
+ return self
51
+ end
63
52
 
64
- self
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
- lockable = self.class.locker.lock(self)
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 lockable was set. We're using this to
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.unlock(lockable) if lockable
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: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
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-13 00:00:00 -07:00
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.txt
62
+ - README.md
63
63
  - LICENSE
64
64
  files:
65
65
  - LICENSE
66
- - README.txt
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
- #