mongo-lock 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +22 -0
- data/README.md +291 -0
- data/Rakefile +8 -0
- data/lib/mongo-lock.rb +296 -0
- data/lib/mongo-lock/configuration.rb +72 -0
- data/lib/mongo-lock/version.rb +5 -0
- data/mongo-lock.gemspec +30 -0
- data/mongoid.yml +6 -0
- data/spec/acquire_spec.rb +184 -0
- data/spec/acquired_spec.rb +52 -0
- data/spec/available_spec.rb +68 -0
- data/spec/clear_expired_spec.rb +20 -0
- data/spec/configuration_spec.rb +238 -0
- data/spec/configure_spec.rb +87 -0
- data/spec/ensure_indexes_spec.rb +25 -0
- data/spec/expired_spec.rb +40 -0
- data/spec/extend_by_spec.rb +133 -0
- data/spec/initialise_spec.rb +36 -0
- data/spec/release_all_spec.rb +78 -0
- data/spec/release_spec.rb +164 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/mongo_helper.rb +19 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MGRiNTY2ZDM1N2YyM2M3MjcwZGNmMTI3ZTVhM2YyMmM1MWNlNWI3Zg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
Y2VlMzFlMDU2MDljMGE3ZTI1NTQwZWE1NDU2OTJmMmMwYjUzOWRmZQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZDU5MmM1YTYxN2Y4ZDhjNDMxYmM5NjQzYjliMmZhN2YzZmIwODQ1NzkwMWVi
|
10
|
+
MmFlMGNhYmE1NTk2ZTI4NDY1M2ZlNWY1MjMxN2E5YjBhNTlmMzEwMWVmZDFk
|
11
|
+
ZTY3YTgyYWJhOWU3YmE3ODE4MmQ0ZjZjZTM2N2M1NTQ0ZTk0Mjc=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTQzNWNiYTYxYWVkZDQ2MjNlZGEwNDYzMTQ2MzBkNTA3OTZkODZmNDczYzEx
|
14
|
+
YmViNzkyN2RlMzU0MDA4MDE5NGMzNzNlMzE2MWUzNTM2YjUyZTFhYTJmZDQ3
|
15
|
+
M2VjYjg0NDQwNWE4YzEyNTFhNzBkMWY4OWUzMDhjNTczOWQxODQ=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
mongo-lock (1.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activesupport (4.0.3)
|
10
|
+
i18n (~> 0.6, >= 0.6.4)
|
11
|
+
minitest (~> 4.2)
|
12
|
+
multi_json (~> 1.3)
|
13
|
+
thread_safe (~> 0.1)
|
14
|
+
tzinfo (~> 0.3.37)
|
15
|
+
atomic (1.1.14)
|
16
|
+
coderay (1.1.0)
|
17
|
+
diff-lcs (1.2.5)
|
18
|
+
fuubar (1.3.2)
|
19
|
+
rspec (>= 2.14.0, < 3.1.0)
|
20
|
+
ruby-progressbar (~> 1.3)
|
21
|
+
i18n (0.6.9)
|
22
|
+
method_source (0.8.2)
|
23
|
+
minitest (4.7.5)
|
24
|
+
multi_json (1.8.4)
|
25
|
+
pry (0.9.12.6)
|
26
|
+
coderay (~> 1.0)
|
27
|
+
method_source (~> 0.8)
|
28
|
+
slop (~> 3.4)
|
29
|
+
rspec (2.14.1)
|
30
|
+
rspec-core (~> 2.14.0)
|
31
|
+
rspec-expectations (~> 2.14.0)
|
32
|
+
rspec-mocks (~> 2.14.0)
|
33
|
+
rspec-core (2.14.7)
|
34
|
+
rspec-expectations (2.14.5)
|
35
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
36
|
+
rspec-mocks (2.14.5)
|
37
|
+
ruby-progressbar (1.4.1)
|
38
|
+
slop (3.4.7)
|
39
|
+
thread_safe (0.1.3)
|
40
|
+
atomic
|
41
|
+
tzinfo (0.3.38)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
activesupport
|
48
|
+
fuubar
|
49
|
+
mongo-lock!
|
50
|
+
pry
|
51
|
+
rspec
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 trak.io Ltd
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
Mongo::Lock
|
2
|
+
==========
|
3
|
+
|
4
|
+
[![Code Climate](https://codeclimate.com/github/trakio/mongo-lock.png)](https://codeclimate.com/github/trakio/mongo-lock)
|
5
|
+
|
6
|
+
Key based pessimistic locking for Ruby and MongoDB. Is this key avaliable? Yes - Lock it for me for a sec will you. No - OK I'll just wait here until its ready.
|
7
|
+
|
8
|
+
It handles timeouts and and vanishing lock owners (such as machine failures)
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'mongo-lock'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
```
|
21
|
+
$ bundle
|
22
|
+
```
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
```
|
27
|
+
$ gem install mongo-lock
|
28
|
+
```
|
29
|
+
|
30
|
+
Build you indexes on any collection that is going to hold locks:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
Mongo::Lock.ensure_indexes # Will use the collection provided to #configure
|
34
|
+
```
|
35
|
+
|
36
|
+
For this to work you must have configured your collection or collections in when intializing locks, in #configure or .configure.
|
37
|
+
|
38
|
+
## Shout outs
|
39
|
+
|
40
|
+
We took quite a bit of inspiration from the [redis versions](https://github.com/mlanett/redis-lock) [of this gem](https://github.com/PatrickTulskie/redis-lock) by [@mlanett](https://github.com/mlanett/redis-lock) and [@PatrickTulskie](https://github.com/PatrickTulskie). If you aren't already using MongoDB or are already using Redis in your stack you probably want to think about using one of them.
|
41
|
+
|
42
|
+
We also looked at [mongo-locking gem](https://github.com/servio/mongo-locking) by [@servio](https://github.com/servio). It was a bit complicated for ours needs, but if you need to lock related and embedded documents rather than just keys it could be what you need.
|
43
|
+
|
44
|
+
## Background
|
45
|
+
|
46
|
+
A lock has an expected lifetime. If the owner of a lock disappears (due to machine failure, network failure, process death), you want the lock to expire and another owner to be able to acquire the lock. At the same time, the owner of a lock should be able to extend its lifetime. Thus, you can acquire a lock with a conservative estimate on lifetime, and extend it as necessary, rather than acquiring the lock with a very long lifetime which will result in long waits in the event of failures.
|
47
|
+
|
48
|
+
A lock has an owner. Mongo::Lock defaults to using an owner id of HOSTNAME:PID:TID.
|
49
|
+
|
50
|
+
## Configuration
|
51
|
+
|
52
|
+
Mongo::Lock makes no effort to help configure the MongoDB connection - that's
|
53
|
+
what the Mongo Ruby Driver is for.
|
54
|
+
|
55
|
+
Configuring Mongo::Lock with the Mongo Ruby Driver would look like this:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Mongo::Lock.configure collection: Mongo::Connection.new("localhost").db("somedb").collection("locks")
|
59
|
+
```
|
60
|
+
|
61
|
+
Or using Mongoid:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
Mongo::Lock.configure collection: Mongoid.database.collection("locks")
|
65
|
+
```
|
66
|
+
|
67
|
+
You can add multiple collections with a hash that can be referenced later using symbols:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
Mongo::Lock.configure collections: { default: Mongoid.database.collection("locks"), other: Mongoid.database.collection("other_locks") }
|
71
|
+
Mongo::Lock.acquire('my_lock') # Locks in the default collection
|
72
|
+
Mongo::Lock.acquire('my_lock', collection: :other) # Locks in the other_locks collection
|
73
|
+
```
|
74
|
+
|
75
|
+
You can also configure using a block:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
Mongo::Lock.configure do |config|
|
79
|
+
config.collections: {
|
80
|
+
default: Mongoid.database.collection("locks"),
|
81
|
+
other: Mongoid.database.collection("other_locks")
|
82
|
+
}
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
### Acquisition timeout_in
|
87
|
+
|
88
|
+
A lock may need more than one attempt to acquire it. Mongo::Lock offers:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
Mongo::Lock.configure do |config|
|
92
|
+
config.timeout_in = false # timeout_in in seconds on acquisition; this defaults to false ie no time limit.
|
93
|
+
config.limit = 100 # The limit on the number of acquisition attempts; this defaults to 100.
|
94
|
+
config.frequency = 1 # Frequency in seconds for acquisition attempts ; this defaults to 1.
|
95
|
+
# acquisition_attempt_frequency can also be given as a proc which will be passed the attempt number
|
96
|
+
config.frequency = Proc.new { |x| x**2 }
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
### Lock Expiry
|
101
|
+
|
102
|
+
A lock will automatically be relinquished once its expiry has passed. Expired locks are cleaned up by [MongoDB's TTL index](http://docs.mongodb.org/manual/tutorial/expire-data/), which may take up to 60 seconds or more depending on load to actually remove expired locks. Expired locks that have not been cleaned out can still be acquire. **You must have built your indexes to ensure expired locks are cleaned out.**
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
Mongo::Lock.configure do |config|
|
106
|
+
config.expires_after = false # timeout_in in seconds for lock expiry; this defaults to 10.
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
You can remove expired locks yourself with:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
Mongo::Lock.clean_expired
|
114
|
+
```
|
115
|
+
|
116
|
+
|
117
|
+
### Raising Errors
|
118
|
+
|
119
|
+
If a lock cannot be acquired, released or extended it will return false, you can set the raise option to true to raise a Mongo::Lock::LockNotAcquiredError or Mongo::Lock::LockNotReleasedError.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
Mongo::Lock.configure do |config|
|
123
|
+
config.raise = true # Whether to raise an error when acquire, release or extend fail.
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
Using .acquire!, #acquire!, .release!, #release!, #extend_by! and #extend! will also raise exceptions instead of returning false.
|
128
|
+
|
129
|
+
### Owner
|
130
|
+
|
131
|
+
By default the owner id will be generated using the following Proc:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
Proc.new { "#{`hostname`.strip}:#{Process.pid}:#{Thread.object_id}" }
|
135
|
+
```
|
136
|
+
|
137
|
+
You can override this with either a Proc that returns any object that responds to to_s, or with any object that responds to #to_s.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
Mongo::Lock.configure do |config|
|
141
|
+
config.owner = ['my', 'owner', 'id']
|
142
|
+
end
|
143
|
+
# Or
|
144
|
+
Mongo::Lock.configure do |config|
|
145
|
+
config.owner = Proc.new { [`hostname`.strip, Process.pid] }
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
Note: Hosts, threads or processes using the same owner can acquire each others locks.
|
150
|
+
|
151
|
+
## Usage
|
152
|
+
|
153
|
+
You can use Mongo::Lock's class methods:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
Mongo::Lock.acquire('my_key', options) do |lock|
|
157
|
+
# Do Something here that needs my_key locked
|
158
|
+
end
|
159
|
+
|
160
|
+
lock = Mongo::Lock.new('my_key', options).acquire
|
161
|
+
# Do Something here that needs my_key locked
|
162
|
+
lock.release
|
163
|
+
# or
|
164
|
+
Mongo::Lock.release('my_key')
|
165
|
+
```
|
166
|
+
|
167
|
+
Or you can initialise your own instance.
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
Mongo::Lock.new('my_key', options).acquire do |lock|
|
171
|
+
# Do Something here that needs my_key locked
|
172
|
+
end
|
173
|
+
|
174
|
+
lock = Mongo::Lock.acquire('my_key', options)
|
175
|
+
# Do Something here that needs my_key locked
|
176
|
+
lock.release
|
177
|
+
# or
|
178
|
+
Mongo::Lock.release('my_key')
|
179
|
+
```
|
180
|
+
|
181
|
+
### Options
|
182
|
+
|
183
|
+
When using Mongo::Lock#acquire, Mongo::Lock#release or Mongo::Lock#new after the key you may overide any of the following options:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
Mongo::Lock.new 'my_key', {
|
187
|
+
collection: Mongo::Connection.new("localhost").db("somedb").collection("locks"), # May also be a symbol if that symbol was provided in the collections hash to Mongo::Lock.configure
|
188
|
+
timeout_in: 10, # timeout_in in seconds on acquisition; this defaults to false ie no time limit.
|
189
|
+
limit: 10, # The limit on the number of acquisition attempts; this defaults to 100.
|
190
|
+
frequency: 2, # Frequency in seconds for acquisition attempts ; this defaults to 1.
|
191
|
+
expires_after: 10,# timeout_in in seconds for lock expiry; this defaults to 10.
|
192
|
+
}
|
193
|
+
```
|
194
|
+
|
195
|
+
### Extending lock
|
196
|
+
|
197
|
+
You can extend a lock by calling Mongo::Lock#extend_by with the number of seconds to extend the lock.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
Mongo::Lock.new 'my_key' do |lock|
|
201
|
+
lock.extend_by 10
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
You can also call Mongo::Lock#extend and it will extend by the lock's expires_after option.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
Mongo::Lock.new 'my_key' do |lock|
|
209
|
+
lock.extend
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
### Check you still hold a lock
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
Mongo::Lock.acquire 'my_key', expires_after: 10 do |lock|
|
217
|
+
sleep 9
|
218
|
+
lock.expired? # False
|
219
|
+
sleep 11
|
220
|
+
lock.expired? # True
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
### Check a key is already locked without acquiring it
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
Mongo::Lock.available? 'my_key'
|
228
|
+
# Or
|
229
|
+
lock = Mongo::Lock.new('my_key')
|
230
|
+
lock.available?
|
231
|
+
```
|
232
|
+
|
233
|
+
### Failures
|
234
|
+
|
235
|
+
If Mongo::Lock#acquire cannot acquire a lock within its configuration limits it will return false.
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
unless Mongo::Lock.acquire 'my_key'
|
239
|
+
# Maybe try again tomorrow
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
If Mongo::Lock#release cannot release a lock because it wasn't acquired it will return false. If it has already been released, or has expired it will do nothing and return true.
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
unless Mongo::Lock.release 'my_key'
|
247
|
+
# Eh somebody else should release it eventually
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
If Mongo::Lock#extend cannot be extended because it has already been released, it is owned by someone else or it was never acquired it will return false.
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
unless lock.extend_by 10
|
255
|
+
# Eh somebody else should release it eventually
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
If the raise error option is set to true or you append ! to the end of the method name and you call any of the acquire, release, extend_by or extend methods they will raise a Mongo::Lock::NotAcquiredError, Mongo::Lock::NotReleasedError or Mongo::Lock::NotExtendedError instead of returning false.
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
begin
|
263
|
+
Mongo::Lock.acquire! 'my_key'
|
264
|
+
rescue Mongo::Lock::LockNotAcquiredError => e
|
265
|
+
# Maybe try again tomorrow
|
266
|
+
end
|
267
|
+
|
268
|
+
# Or
|
269
|
+
|
270
|
+
begin
|
271
|
+
Mongo::Lock.acquire 'my_key', raise: true
|
272
|
+
rescue Mongo::Lock::LockNotAcquiredError => e
|
273
|
+
# Maybe try again tomorrow
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
## Contributors
|
278
|
+
|
279
|
+
Matthew Spence (msaspence)
|
280
|
+
|
281
|
+
The bulk of this gem has been developed for and by [trak.io](http://trak.io)
|
282
|
+
|
283
|
+
[![trak.io](http://trak.io/assets/images/logo@2x.png)](http://trak.io)
|
284
|
+
|
285
|
+
## Contributing
|
286
|
+
|
287
|
+
1. Fork it
|
288
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
289
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
290
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
291
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/mongo-lock.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'mongo-lock/configuration'
|
2
|
+
|
3
|
+
module Mongo
|
4
|
+
class Lock
|
5
|
+
|
6
|
+
class NotAcquiredError < StandardError ; end
|
7
|
+
class NotReleasedError < StandardError ; end
|
8
|
+
class NotExtendedError < StandardError ; end
|
9
|
+
|
10
|
+
attr_accessor :configuration
|
11
|
+
attr_accessor :key
|
12
|
+
attr_accessor :acquired
|
13
|
+
attr_accessor :expires_at
|
14
|
+
attr_accessor :released
|
15
|
+
|
16
|
+
def self.configure options = {}, &block
|
17
|
+
defaults = {
|
18
|
+
timeout_in: 10,
|
19
|
+
limit: 100,
|
20
|
+
frequency: 1,
|
21
|
+
expires_after: 10,
|
22
|
+
raise: false,
|
23
|
+
owner: Proc.new { "#{`hostname`.strip}:#{Process.pid}:#{Thread.object_id}" }
|
24
|
+
}
|
25
|
+
defaults = defaults.merge(@@default_configuration) if defined?(@@default_configuration) && @@default_configuration
|
26
|
+
@@default_configuration = Configuration.new(defaults, options, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.configuration
|
30
|
+
if defined? @@default_configuration
|
31
|
+
@@default_configuration
|
32
|
+
else
|
33
|
+
@@default_configuration = configure
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.release_all options = {}
|
38
|
+
if options.include? :collection
|
39
|
+
release_collection configuration.collection(options[:collection]), options[:owner]
|
40
|
+
else
|
41
|
+
configuration.collections.each_pair do |key,collection|
|
42
|
+
release_collection collection, options[:owner]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.release_collection collection, owner=nil
|
48
|
+
selector = if owner then { owner: owner } else {} end
|
49
|
+
collection.remove(selector)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.init_and_send key, options = {}, method
|
53
|
+
lock = self.new(key, options)
|
54
|
+
lock.send(method)
|
55
|
+
lock
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.acquire key, options = {}
|
59
|
+
init_and_send key, options, :acquire
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.release key, options = {}
|
63
|
+
init_and_send key, options, :release
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.acquire! key, options = {}
|
67
|
+
init_and_send key, options, :acquire!
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.release! key, options = {}
|
71
|
+
init_and_send key, options, :release!
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.available? key, options = {}
|
75
|
+
init_and_send key, options, :available?
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.ensure_indexes
|
79
|
+
configuration.collections.each_pair do |key, collection|
|
80
|
+
collection.create_index([
|
81
|
+
['key', Mongo::ASCENDING],
|
82
|
+
['owner', Mongo::ASCENDING],
|
83
|
+
['expires_at', Mongo::ASCENDING]
|
84
|
+
])
|
85
|
+
collection.create_index([['ttl', Mongo::ASCENDING]],{ expireAfterSeconds: 0 })
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.clear_expired
|
90
|
+
configuration.collections.each_pair do |key,collection|
|
91
|
+
collection.remove expires_at: { '$lt' => Time.now }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize key, options = {}
|
96
|
+
self.configuration = Configuration.new self.class.configuration.to_hash, options
|
97
|
+
self.key = key
|
98
|
+
acquire_if_acquired
|
99
|
+
end
|
100
|
+
|
101
|
+
def configure options = {}, &block
|
102
|
+
self.configuration = Configuration.new self.configuration.to_hash, options
|
103
|
+
yield self.configuration if block_given?
|
104
|
+
end
|
105
|
+
|
106
|
+
def acquire options = {}
|
107
|
+
options = configuration.to_hash.merge options
|
108
|
+
i = 1
|
109
|
+
time_spent = 0
|
110
|
+
|
111
|
+
loop do
|
112
|
+
# If timeout has expired
|
113
|
+
if options[:timeout_in] && options[:timeout_in] < time_spent
|
114
|
+
return raise_or_false options
|
115
|
+
|
116
|
+
# If limit has expired
|
117
|
+
elsif options[:limit] && options[:limit] < i
|
118
|
+
return raise_or_false options
|
119
|
+
|
120
|
+
# If there is an existing lock
|
121
|
+
elsif existing_lock = find_or_insert(options)
|
122
|
+
|
123
|
+
# If the lock is owned by me
|
124
|
+
if existing_lock['owner'] == options[:owner]
|
125
|
+
self.acquired = true
|
126
|
+
extend_by options[:expires_after]
|
127
|
+
return true
|
128
|
+
end
|
129
|
+
|
130
|
+
# If the lock was acquired
|
131
|
+
else
|
132
|
+
self.acquired = true
|
133
|
+
return true
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
if options[:frequency].is_a? Proc
|
138
|
+
frequency = options[:frequency].call(i)
|
139
|
+
else
|
140
|
+
frequency = options[:frequency]
|
141
|
+
end
|
142
|
+
sleep frequency
|
143
|
+
time_spent += frequency
|
144
|
+
i += 1
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def acquire! options = {}
|
149
|
+
options[:raise] = true
|
150
|
+
acquire options
|
151
|
+
end
|
152
|
+
|
153
|
+
def release options = {}
|
154
|
+
options = configuration.to_hash.merge options
|
155
|
+
|
156
|
+
# If the lock has already been released
|
157
|
+
if released?
|
158
|
+
return true
|
159
|
+
|
160
|
+
# If the lock has expired its as good as released
|
161
|
+
elsif expired?
|
162
|
+
self.released = true
|
163
|
+
self.acquired = false
|
164
|
+
return true
|
165
|
+
|
166
|
+
# We must have acquired the lock to release it
|
167
|
+
elsif !acquired?
|
168
|
+
if acquire options.merge(raise: false)
|
169
|
+
return release options
|
170
|
+
else
|
171
|
+
return raise_or_false options, NotReleasedError
|
172
|
+
end
|
173
|
+
|
174
|
+
else
|
175
|
+
self.released = true
|
176
|
+
self.acquired = false
|
177
|
+
collection.remove key: key, owner: options[:owner]
|
178
|
+
return true
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def release! options = {}
|
183
|
+
options[:raise] = true
|
184
|
+
release options
|
185
|
+
end
|
186
|
+
|
187
|
+
def raise_or_false options, error = NotAcquiredError
|
188
|
+
raise error if options[:raise]
|
189
|
+
false
|
190
|
+
end
|
191
|
+
|
192
|
+
def find_or_insert options
|
193
|
+
to_expire_at = Time.now + options[:expires_after]
|
194
|
+
existing_lock = collection.find_and_modify({
|
195
|
+
query: query,
|
196
|
+
update: {
|
197
|
+
'$setOnInsert' => {
|
198
|
+
key: key,
|
199
|
+
owner: options[:owner],
|
200
|
+
expires_at: to_expire_at,
|
201
|
+
ttl: to_expire_at
|
202
|
+
}
|
203
|
+
},
|
204
|
+
upsert: true
|
205
|
+
})
|
206
|
+
|
207
|
+
if existing_lock
|
208
|
+
self.expires_at = existing_lock['expires_at']
|
209
|
+
else
|
210
|
+
self.expires_at = to_expire_at
|
211
|
+
end
|
212
|
+
|
213
|
+
existing_lock
|
214
|
+
end
|
215
|
+
|
216
|
+
def extend_by time, options = {}
|
217
|
+
options = configuration.to_hash.merge options
|
218
|
+
|
219
|
+
# Can't extend a lock that hasn't been acquired
|
220
|
+
if !acquired?
|
221
|
+
return raise_or_false options, NotExtendedError
|
222
|
+
|
223
|
+
# Can't extend a lock that has started
|
224
|
+
elsif expired?
|
225
|
+
return raise_or_false options, NotExtendedError
|
226
|
+
|
227
|
+
else
|
228
|
+
to_expire_at = expires_at + time
|
229
|
+
existing_lock = collection.find_and_modify({
|
230
|
+
query: query,
|
231
|
+
update: {
|
232
|
+
'$set' => {
|
233
|
+
key: key,
|
234
|
+
owner: options[:owner],
|
235
|
+
expires_at: to_expire_at,
|
236
|
+
ttl: to_expire_at
|
237
|
+
}
|
238
|
+
},
|
239
|
+
upsert: true
|
240
|
+
})
|
241
|
+
true
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def extend options = {}
|
246
|
+
time = configuration.to_hash.merge(options)[:expires_after]
|
247
|
+
extend_by time, options
|
248
|
+
end
|
249
|
+
|
250
|
+
def extend_by! time, options = {}
|
251
|
+
options[:raise] = true
|
252
|
+
extend_by time, options
|
253
|
+
end
|
254
|
+
|
255
|
+
def extend! options = {}
|
256
|
+
options[:raise] = true
|
257
|
+
extend options
|
258
|
+
end
|
259
|
+
|
260
|
+
def available? options = {}
|
261
|
+
options = configuration.to_hash.merge options
|
262
|
+
existing_lock = collection.find(query).first
|
263
|
+
!existing_lock || existing_lock['owner'] == options[:owner]
|
264
|
+
end
|
265
|
+
|
266
|
+
def query
|
267
|
+
{
|
268
|
+
key: key,
|
269
|
+
expires_at: { '$gt' => Time.now }
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
def acquired?
|
274
|
+
!!acquired && !expired?
|
275
|
+
end
|
276
|
+
|
277
|
+
def expired?
|
278
|
+
!!(expires_at && expires_at < Time.now)
|
279
|
+
end
|
280
|
+
|
281
|
+
def released?
|
282
|
+
!!released
|
283
|
+
end
|
284
|
+
|
285
|
+
def acquire_if_acquired
|
286
|
+
if (collection.find({
|
287
|
+
key: key,
|
288
|
+
owner: configuration.owner,
|
289
|
+
expires_at: { '$gt' => Time.now }
|
290
|
+
}).count > 0)
|
291
|
+
self.acquired = true
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
end
|
296
|
+
end
|