mongo-locking 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|