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 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
- #