mongo-locking 0.0.1
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 +1 -0
- data/README.txt +42 -0
- data/Rakefile +3 -0
- data/lib/mongo/locking.rb +95 -0
- data/lib/mongo/locking/locker.rb +274 -0
- data/lib/mongo/locking/model_methods.rb +101 -0
- metadata +106 -0
data/LICENSE
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Still working this out.
|
data/README.txt
ADDED
@@ -0,0 +1,42 @@
|
|
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
|
+
#
|
data/Rakefile
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
##
|
2
|
+
## Mongo Locking Library
|
3
|
+
##
|
4
|
+
#
|
5
|
+
# Despite the Locker being placed on the model classes, the dependency graph is
|
6
|
+
# actually instance-based, thus the graph can only be resolved during runtime.
|
7
|
+
#
|
8
|
+
# Locking takes an instance to start with, optionally walks the graph through
|
9
|
+
# parent "pointers" (procs whose arbitrary function produces the parent
|
10
|
+
# lockable), then does the atomic incr/decr dance with the root lockable's key.
|
11
|
+
#
|
12
|
+
# TODO: More docs. TBC.
|
13
|
+
|
14
|
+
require 'mongo'
|
15
|
+
require 'active_support/core_ext/module/delegation'
|
16
|
+
|
17
|
+
module Mongo
|
18
|
+
module Locking
|
19
|
+
extend self
|
20
|
+
|
21
|
+
module Exceptions
|
22
|
+
class Error < RuntimeError; end
|
23
|
+
class ArgumentError < Error; end # bad param
|
24
|
+
class InvalidConfig < Error; end # something bogus in config[]
|
25
|
+
class CircularLock < Error; end # circular lock reference
|
26
|
+
class LockTimeout < Error; end # lock failed, retries unsuccessful
|
27
|
+
class LockFailure < Error; end # lock failed, something bad happened
|
28
|
+
end
|
29
|
+
include Exceptions
|
30
|
+
|
31
|
+
attr_accessor :logger, :collection
|
32
|
+
delegate :debug, :info, :warn, :error, :fatal, :to => :logger, :allow_nil => true
|
33
|
+
|
34
|
+
def included(klass)
|
35
|
+
klass.send(:include, ModelMethods)
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
## Configuration
|
40
|
+
##
|
41
|
+
|
42
|
+
def configure(opts = {})
|
43
|
+
opts.each { |k, v| send("#{k}=", v) }
|
44
|
+
return self
|
45
|
+
end
|
46
|
+
|
47
|
+
def collection=(new)
|
48
|
+
@collection = new
|
49
|
+
ensure_indices if @collection.kind_of? Mongo::Collection
|
50
|
+
return @collection
|
51
|
+
end
|
52
|
+
|
53
|
+
# Allow for a proc to be initially set as the collection, in case of
|
54
|
+
# delayed loading/configuration/whatever. It'll only be materialized
|
55
|
+
# when first accessed, which is presumably either when ensure_indices is
|
56
|
+
# called imperatively, or more likely upon the first lock call.
|
57
|
+
def collection
|
58
|
+
if @collection.kind_of? Proc
|
59
|
+
@collection = @collection.call
|
60
|
+
ensure_indices if @collection.kind_of? Mongo::Collection
|
61
|
+
end
|
62
|
+
return @collection
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
## Document Indices
|
67
|
+
##
|
68
|
+
#
|
69
|
+
# A Mongoid model would look like:
|
70
|
+
# field :scope, :type => String
|
71
|
+
# field :key, :type => String
|
72
|
+
# field :refcount, :type => Integer, :default => 0
|
73
|
+
# field :expire_at, :type => DateTime
|
74
|
+
# index [ [ :scope, Mongo::ASCENDING ], [ :key, Mongo::ASCENDING ] ], :unique => true
|
75
|
+
# index :refcount
|
76
|
+
# index :expire_at
|
77
|
+
|
78
|
+
LOCK_INDICES = {
|
79
|
+
[["scope", Mongo::ASCENDING], ["key", Mongo::ASCENDING]] => { :unique => true, :background => true },
|
80
|
+
[["refcount", Mongo::ASCENDING]] => { :unique => false, :background => true },
|
81
|
+
[["expire_at", Mongo::ASCENDING]] => { :unique => false, :background => true },
|
82
|
+
}
|
83
|
+
|
84
|
+
def ensure_indices
|
85
|
+
LOCK_INDICES.each do |spec, opts|
|
86
|
+
@collection.ensure_index(spec, opts)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end # Locking
|
91
|
+
end # Mongo
|
92
|
+
|
93
|
+
require 'mongo/locking/locker'
|
94
|
+
require 'mongo/locking/model_methods'
|
95
|
+
|
@@ -0,0 +1,274 @@
|
|
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
|
+
require 'mongo/locking'
|
15
|
+
require 'active_support/core_ext/numeric/time'
|
16
|
+
require 'active_support/core_ext/object/blank'
|
17
|
+
|
18
|
+
module Mongo
|
19
|
+
module Locking
|
20
|
+
|
21
|
+
class Locker
|
22
|
+
include Exceptions
|
23
|
+
|
24
|
+
attr_reader :config
|
25
|
+
|
26
|
+
DEFAULT_OPTIONS = {
|
27
|
+
:max_retries => 5,
|
28
|
+
:first_retry_interval => 0.2.seconds,
|
29
|
+
:max_retry_interval => 5.seconds,
|
30
|
+
:max_lifetime => 10.minutes,
|
31
|
+
}
|
32
|
+
|
33
|
+
def initialize(opts = {})
|
34
|
+
@config = DEFAULT_OPTIONS.merge(opts)
|
35
|
+
@refcount_key = "mongo_locking_refcounts_#{@config[:class_name]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def refcounts
|
39
|
+
Thread.current[@refcount_key] ||= Hash.new(0)
|
40
|
+
end
|
41
|
+
|
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)
|
55
|
+
lockable = root_for(from)
|
56
|
+
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
|
78
|
+
|
79
|
+
refcounts[key] += 1
|
80
|
+
if refcounts[key] > 1
|
81
|
+
Locking.info "acquire: re-using lock for #{name}##{refcounts[key]}"
|
82
|
+
return true
|
83
|
+
end
|
84
|
+
|
85
|
+
target = { :scope => scope, :key => key }
|
86
|
+
interval = self.config[:first_retry_interval]
|
87
|
+
retries = 0
|
88
|
+
|
89
|
+
Locking.debug "acquire: attempting lock of #{name}"
|
90
|
+
|
91
|
+
begin
|
92
|
+
a_lock = atomic_inc(target, { :refcount => 1 })
|
93
|
+
refcount = a_lock['refcount']
|
94
|
+
|
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
|
+
# If the refcount is 0 or somehow less than 0, after we just
|
103
|
+
# incremented, then retry without counting against the max.
|
104
|
+
if refcount < 1
|
105
|
+
retries -= 1
|
106
|
+
Locking.debug "acquire: refcount #{refcount}, unexpected state"
|
107
|
+
raise LockFailure
|
108
|
+
end
|
109
|
+
|
110
|
+
# If recount is greater than 1, we lost the race. Decrement
|
111
|
+
# and try again.
|
112
|
+
if refcount > 1
|
113
|
+
atomic_inc(target, { :refcount => -1 })
|
114
|
+
Locking.debug "acquire: refcount #{refcount}, race lost"
|
115
|
+
raise LockFailure
|
116
|
+
end
|
117
|
+
|
118
|
+
rescue LockFailure => e
|
119
|
+
retries += 1
|
120
|
+
if retries >= self.config[:max_retries]
|
121
|
+
refcounts[key] -= 1
|
122
|
+
raise LockTimeout, "unable to acquire lock #{name}"
|
123
|
+
end
|
124
|
+
|
125
|
+
Locking.warn "acquire: #{name} refcount #{refcount}, retry #{retries} for lock"
|
126
|
+
|
127
|
+
sleep(interval.to_f)
|
128
|
+
interval = [self.config[:max_retry_interval].to_f, interval * 2].min
|
129
|
+
retry
|
130
|
+
|
131
|
+
rescue => e
|
132
|
+
refcounts[key] -= 1
|
133
|
+
|
134
|
+
log_exception(e)
|
135
|
+
raise LockFailure, "unable to acquire lock #{name}"
|
136
|
+
end
|
137
|
+
|
138
|
+
Locking.info "acquire: #{name} locked (#{refcount})"
|
139
|
+
|
140
|
+
return true
|
141
|
+
end
|
142
|
+
|
143
|
+
def release(scope, key)
|
144
|
+
name = scope + "/" + key
|
145
|
+
|
146
|
+
refcounts[key] -= 1
|
147
|
+
if refcounts[key] > 0
|
148
|
+
Locking.info "release: re-using lock for #{name}##{refcounts[key]}"
|
149
|
+
return true
|
150
|
+
end
|
151
|
+
|
152
|
+
target = { :scope => scope, :key => key }
|
153
|
+
|
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
|
175
|
+
end
|
176
|
+
|
177
|
+
rescue => e
|
178
|
+
log_exception(e)
|
179
|
+
raise LockFailure, "unable to release lock #{name}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def root_for(from)
|
184
|
+
lockable = from
|
185
|
+
visited = Set.new([lockable.class])
|
186
|
+
|
187
|
+
while parent = lockable.class.locker.parent_for(lockable)
|
188
|
+
lockable = parent
|
189
|
+
|
190
|
+
if visited.include? lockable.class
|
191
|
+
raise CircularLock, "already visited #{lockable.class} (#{visited.inspect})"
|
192
|
+
end
|
193
|
+
|
194
|
+
visited << lockable.class
|
195
|
+
end
|
196
|
+
|
197
|
+
raise InvalidConfig, "root #{lockable.inspect} is not lockable" unless lockable.class.locker.is_root?
|
198
|
+
|
199
|
+
return lockable
|
200
|
+
end
|
201
|
+
|
202
|
+
def key_for(lockable)
|
203
|
+
return case key = self.config[:key]
|
204
|
+
when Proc then key.call(self).to_s
|
205
|
+
when Symbol then lockable.send(key).to_s
|
206
|
+
else raise InvalidConfig, "unknown key type #{key.inspect}"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def scope_for(lockable)
|
211
|
+
return case scope = self.config[:scope]
|
212
|
+
when Proc then scope.call(lockable).to_s
|
213
|
+
when Symbol then scope.to_s
|
214
|
+
when String then scope
|
215
|
+
else raise InvalidConfig, "unknown scope type #{scope.inspect}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def parent_for(lockable)
|
220
|
+
return case parent = self.config[:parent]
|
221
|
+
when Proc then parent.call(self)
|
222
|
+
when Symbol then lockable.send(parent)
|
223
|
+
when NilClass then nil
|
224
|
+
else raise InvalidConfig, "unknown parent type #{parent.inspect}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def is_root?
|
229
|
+
self.config[:parent].blank?
|
230
|
+
end
|
231
|
+
|
232
|
+
private
|
233
|
+
|
234
|
+
def log_exception(e)
|
235
|
+
Locking.error e.inspect + " " + e.backtrace[0..4].inspect
|
236
|
+
end
|
237
|
+
|
238
|
+
# Notes:
|
239
|
+
#
|
240
|
+
# - search_hash contains mapping of search_key => search_value
|
241
|
+
# - search_key is the key used to uniquely identify the document
|
242
|
+
# to update
|
243
|
+
# - search_key MUST be the sharding key if we do sharding
|
244
|
+
# later. This ensures that "$atomic" still meaningful
|
245
|
+
#
|
246
|
+
# Mongo:
|
247
|
+
#
|
248
|
+
# - $atomic actually means "write isolation". '$atomic'=>1 means
|
249
|
+
# we are the only writer to this document.
|
250
|
+
# - '$inc' means "atomic increment"
|
251
|
+
# - set expire_at for future async/lazy lock reaping
|
252
|
+
|
253
|
+
def atomic_inc(search_hash, update_hash)
|
254
|
+
return Locking.collection.find_and_modify({
|
255
|
+
:new => true, # Return the updated document
|
256
|
+
:upsert => true, # Update if document exists, insert if not
|
257
|
+
:query => search_hash.merge({'$atomic' => 1}),
|
258
|
+
:update => {
|
259
|
+
'$inc' => update_hash.dup,
|
260
|
+
'$set' => { 'expire_at' => self.config[:max_lifetime].from_now }
|
261
|
+
},
|
262
|
+
})
|
263
|
+
end
|
264
|
+
|
265
|
+
def atomic_delete(search_hash)
|
266
|
+
return Locking.collection.find_and_modify({
|
267
|
+
:query => search_hash.merge({'$atomic' => 1}),
|
268
|
+
:remove => true,
|
269
|
+
})
|
270
|
+
end
|
271
|
+
|
272
|
+
end # Locker
|
273
|
+
end # Locking
|
274
|
+
end # Mongo
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/array/extract_options'
|
3
|
+
require 'mongo/locking'
|
4
|
+
|
5
|
+
module Mongo
|
6
|
+
module Locking
|
7
|
+
|
8
|
+
module ModelMethods
|
9
|
+
extend ::ActiveSupport::Concern
|
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
|
+
module ClassMethods
|
20
|
+
|
21
|
+
# Options:
|
22
|
+
#
|
23
|
+
# :key => Symbol (sent to self), Proc (passed self) -> result.to_s
|
24
|
+
# :scope => String, Symbol (default self.name)
|
25
|
+
#
|
26
|
+
# :max_retries => default 5
|
27
|
+
# :first_retry_interval => default 0.2.seconds
|
28
|
+
# :max_retry_interval => default 5.seconds
|
29
|
+
# :max_lifetime => default 1.minute
|
30
|
+
#
|
31
|
+
def lockable!(opts = {})
|
32
|
+
key = opts[:key] ||= :id
|
33
|
+
scope = opts[:scope] ||= self.name
|
34
|
+
|
35
|
+
raise ArgumentError, "locker already defined" if self.locker
|
36
|
+
raise ArgumentError, "locker key must be a Proc or Symbol" unless [Proc, Symbol].include?(key.class)
|
37
|
+
raise ArgumentError, "locker scope must be a Proc, Symbol or String" unless [Proc, Symbol, String].include?(scope.class)
|
38
|
+
|
39
|
+
opts[:class_name] = self.name
|
40
|
+
@locker = Locker.new(opts)
|
41
|
+
|
42
|
+
Locking.debug "#{self.name} is lockable (#{opts.inspect})"
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Options:
|
48
|
+
#
|
49
|
+
# :parent => instance of lockable parent
|
50
|
+
#
|
51
|
+
def locked_by!(*args, &parent)
|
52
|
+
opts = args.extract_options!
|
53
|
+
parent = opts[:parent] ||= parent || args.first
|
54
|
+
|
55
|
+
raise ArgumentError, "locker already defined" if self.locker
|
56
|
+
raise ArgumentError, "parent reference must be a Proc or Symbol" unless [Proc, Symbol].include?(parent.class)
|
57
|
+
|
58
|
+
opts[:class_name] = self.name
|
59
|
+
|
60
|
+
@locker = Locker.new(opts)
|
61
|
+
|
62
|
+
Locking.debug "#{self.name} is locked_by (#{opts.inspect})"
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
end # ClassMethods
|
68
|
+
|
69
|
+
module InstanceMethods
|
70
|
+
|
71
|
+
# Main closure-based lock method. opts remains present for
|
72
|
+
# future utility.
|
73
|
+
def lock(opts = {})
|
74
|
+
raise ArgumentError, "#{self.class.name}#lock requires a block" unless block_given?
|
75
|
+
|
76
|
+
lockable = self.class.locker.lock(self)
|
77
|
+
|
78
|
+
return yield
|
79
|
+
|
80
|
+
rescue => e
|
81
|
+
Locking.error "#{self.class.name}#lock failed"
|
82
|
+
raise e
|
83
|
+
|
84
|
+
ensure
|
85
|
+
# Only unlock if lockable was set. We're using this to
|
86
|
+
# distinguish between an exception from the yield vs. an
|
87
|
+
# exception from our own locking code. Doing it in an
|
88
|
+
# ensure block makes us defensible against a return from
|
89
|
+
# within the closure, too.
|
90
|
+
#
|
91
|
+
# Calling lockable's locker instead of self potentially
|
92
|
+
# saves us the cost of "find root lockable" that locker
|
93
|
+
# would perform.
|
94
|
+
lockable.class.locker.unlock(lockable) if lockable
|
95
|
+
end
|
96
|
+
|
97
|
+
end # InstanceMethods
|
98
|
+
|
99
|
+
end # ModelMethods
|
100
|
+
end # Locking
|
101
|
+
end # Mongo
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mongo-locking
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jordan Ritter
|
14
|
+
- Brendan Baldwin
|
15
|
+
- Yanzhu Du
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2011-07-13 00:00:00 -07:00
|
21
|
+
default_executable:
|
22
|
+
dependencies:
|
23
|
+
- !ruby/object:Gem::Dependency
|
24
|
+
name: mongo
|
25
|
+
prerelease: false
|
26
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ~>
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
hash: 25
|
32
|
+
segments:
|
33
|
+
- 1
|
34
|
+
- 3
|
35
|
+
- 1
|
36
|
+
version: 1.3.1
|
37
|
+
type: :runtime
|
38
|
+
version_requirements: *id001
|
39
|
+
- !ruby/object:Gem::Dependency
|
40
|
+
name: active_support
|
41
|
+
prerelease: false
|
42
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
hash: 15
|
48
|
+
segments:
|
49
|
+
- 3
|
50
|
+
- 0
|
51
|
+
- 4
|
52
|
+
version: 3.0.4
|
53
|
+
type: :runtime
|
54
|
+
version_requirements: *id002
|
55
|
+
description: A mixin DSL for implementing cross-process mutexes/locks using MongoDB
|
56
|
+
email: jpr5@serv.io
|
57
|
+
executables: []
|
58
|
+
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files:
|
62
|
+
- README.txt
|
63
|
+
- LICENSE
|
64
|
+
files:
|
65
|
+
- LICENSE
|
66
|
+
- README.txt
|
67
|
+
- Rakefile
|
68
|
+
- lib/mongo/locking/locker.rb
|
69
|
+
- lib/mongo/locking/model_methods.rb
|
70
|
+
- lib/mongo/locking.rb
|
71
|
+
has_rdoc: true
|
72
|
+
homepage: http://github.com/servio/mongo-locking
|
73
|
+
licenses: []
|
74
|
+
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 3
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.6.2
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: A mixin DSL for implementing cross-process mutexes/locks using MongoDB
|
105
|
+
test_files: []
|
106
|
+
|