mongoid-locker 0.2.1 → 0.3.0
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.
- checksums.yaml +7 -0
- data/.rubocop.yml +38 -0
- data/.travis.yml +12 -20
- data/CHANGELOG.md +17 -1
- data/CONTRIBUTING.md +19 -0
- data/Gemfile +14 -9
- data/Guardfile +1 -1
- data/README.md +3 -18
- data/Rakefile +11 -9
- data/VERSION +1 -1
- data/lib/mongoid/locker.rb +59 -46
- data/lib/mongoid/locker/wrapper.rb +7 -7
- data/mongoid-locker.gemspec +22 -21
- data/spec/database2.yml +4 -0
- data/spec/{database.yml → database3.yml} +0 -4
- data/spec/database4.yml +7 -0
- data/spec/mongoid-locker_spec.rb +121 -62
- data/spec/spec_helper.rb +5 -2
- metadata +32 -59
- data/Appraisals +0 -7
- data/Gemfile.lock +0 -83
- data/gemfiles/mongoid2.gemfile +0 -24
- data/gemfiles/mongoid3.gemfile +0 -24
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a80f5cc223862e305b57a190f0713f9b40d98aa2
|
4
|
+
data.tar.gz: 5e7b035e9de4e47acb180f8737aad225fef38170
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 21b22b0c0e1902582d62565efa1ab8d38571960ab915756de31bd2ff9f021c5cab4e9e5bd49bffee967d392a0ef457e572c0a398db88560155124d9df29ffc62
|
7
|
+
data.tar.gz: 582144c90fde1230f723f610f76153ffd475c1005fc5162891c88cd1ed38651e45f42432679a8b46b4682b9c08fe82f950a606128fadd03e7666efb51b4f03f0
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
AllCops:
|
2
|
+
Excludes:
|
3
|
+
- vendor/**
|
4
|
+
- bin/**
|
5
|
+
- mongoid-locker.gemspec
|
6
|
+
|
7
|
+
LineLength:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
MethodLength:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
ClassLength:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Documentation:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Encoding:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Blocks:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
FileName:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
RaiseArgs:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
PredicateName:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
DoubleNegation:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
TrivialAccessors:
|
38
|
+
Enabled: false
|
data/.travis.yml
CHANGED
@@ -1,24 +1,16 @@
|
|
1
1
|
language: ruby
|
2
|
+
|
2
3
|
rvm:
|
3
|
-
- 1.8.7
|
4
|
-
- 1.9.2
|
5
4
|
- 1.9.3
|
6
|
-
-
|
7
|
-
- jruby-19mode
|
8
|
-
- rbx-
|
9
|
-
|
5
|
+
- 2.1.2
|
6
|
+
- jruby-19mode
|
7
|
+
- rbx-2
|
8
|
+
|
10
9
|
services: mongodb
|
11
|
-
|
12
|
-
|
13
|
-
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
gemfile: gemfiles/mongoid3.gemfile
|
19
|
-
- rvm: 1.9.2
|
20
|
-
gemfile: gemfiles/mongoid3.gemfile
|
21
|
-
- rvm: jruby-18mode
|
22
|
-
gemfile: gemfiles/mongoid3.gemfile
|
23
|
-
- rvm: rbx-18mode
|
24
|
-
gemfile: gemfiles/mongoid3.gemfile
|
10
|
+
|
11
|
+
env:
|
12
|
+
- MONGOID_VERSION=2
|
13
|
+
- MONGOID_VERSION=3
|
14
|
+
- MONGOID_VERSION=4
|
15
|
+
|
16
|
+
cache: bundler
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
3
|
+
## 0.3.0 ([diff](https://github.com/afeld/mongoid-locker/compare/v0.2.1...v0.3.0?w=1))
|
4
|
+
|
5
|
+
* change exception class to be `Mongoid::Locker::LockError` - #8
|
6
|
+
* drop support for Rubinius 1.8-mode, since it seems to be [broken w/ Mongoid 2.6](https://travis-ci.org/mongoid/mongoid/jobs/4594000)
|
7
|
+
* relax dependency on Mongoid - #12
|
8
|
+
* add Mongoid 4 support
|
9
|
+
* drop support for Ruby 1.8.x
|
10
|
+
* got rid of appraisal for testing multiple Mongoid versions
|
11
|
+
* added Rubocop, Ruby style linter
|
12
|
+
* fixed `:has_lock?` to always return a boolean
|
13
|
+
* upgraded RSpec to 3.x
|
14
|
+
|
15
|
+
Thanks to @mooremo, @yanowitz and @nchainani (#9):
|
16
|
+
|
17
|
+
* add `:retries` option to attempt to grab a lock multiple times - #2
|
18
|
+
* add `:retry_sleep` to override duration between lock attempts
|
19
|
+
* reload document after acquiring a lock by default, which can be disabled with `:reload => false`
|
4
20
|
|
5
21
|
## 0.2.1 ([diff](https://github.com/afeld/mongoid-locker/compare/v0.2.0...v0.2.1?w=1))
|
6
22
|
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
Pull requests are welcome. To set up:
|
4
|
+
|
5
|
+
$ bundle install
|
6
|
+
|
7
|
+
To run tests:
|
8
|
+
|
9
|
+
$ rake
|
10
|
+
|
11
|
+
To run tests for Mongoid 3:
|
12
|
+
|
13
|
+
$ rm Gemfile.lock
|
14
|
+
$ MONGOID_VERSION=3 bundle install
|
15
|
+
$ MONGOID_VERSION=3 rake
|
16
|
+
|
17
|
+
To auto-run tests as you code:
|
18
|
+
|
19
|
+
$ bundle exec guard
|
data/Gemfile
CHANGED
@@ -1,22 +1,27 @@
|
|
1
1
|
source 'http://rubygems.org'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
case version = ENV['MONGOID_VERSION'] || '~> 4.0'
|
4
|
+
when /4/
|
5
|
+
gem 'mongoid', '~> 4.0'
|
6
|
+
when /3/
|
7
|
+
gem 'mongoid', '~> 3.1'
|
8
|
+
when /2/
|
9
|
+
gem 'bson_ext', :platforms => :ruby
|
10
|
+
gem 'mongoid', '~> 2.4'
|
11
|
+
else
|
12
|
+
gem 'mongoid', version
|
13
|
+
end
|
7
14
|
|
8
15
|
group :development do
|
9
|
-
gem 'rspec', '~>
|
16
|
+
gem 'rspec', '~> 3.0'
|
10
17
|
gem 'bundler', '~> 1.1'
|
11
18
|
gem 'jeweler', '~> 1.8'
|
12
19
|
|
13
20
|
gem 'guard-rspec'
|
21
|
+
gem 'rb-fsevent', '~> 0.9.1'
|
14
22
|
end
|
15
23
|
|
16
24
|
group :development, :test do
|
17
|
-
gem 'bson_ext', :platforms => :ruby
|
18
|
-
|
19
25
|
gem 'rake'
|
20
|
-
|
21
|
-
gem 'appraisal', :git => 'git://github.com/thoughtbot/appraisal.git', :ref => 'ad2aeb99649f6a78f78be5009fb50306f06eaa9f'
|
26
|
+
gem 'rubocop', '0.24.0'
|
22
27
|
end
|
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
# mongoid-locker [](http://travis-ci.org/afeld/mongoid-locker)
|
1
|
+
# mongoid-locker [](http://travis-ci.org/afeld/mongoid-locker) [](https://codeclimate.com/github/afeld/mongoid-locker)
|
2
2
|
|
3
3
|
Document-level locking for MongoDB via Mongoid. The need arose at [Jux](https://jux.com) from multiple processes on multiple servers trying to act upon the same document and stepping on each other's toes. Mongoid-Locker is an easy way to ensure only one process can perform a certain operation on a document at a time.
|
4
4
|
|
5
|
-
[Tested](http://travis-ci.org/afeld/mongoid-locker) against MRI 1.
|
5
|
+
[Tested](http://travis-ci.org/afeld/mongoid-locker) against MRI 1.9.3, 2.0.0 and 2.1.2, Rubinius 2.x, and JRuby 1.9 with Mongoid 2, 3 and 4 ([where supported](http://travis-ci.org/#!/afeld/mongoid-locker)).
|
6
6
|
|
7
7
|
## Usage
|
8
8
|
|
@@ -35,10 +35,7 @@ queue_item.with_lock do
|
|
35
35
|
end
|
36
36
|
```
|
37
37
|
|
38
|
-
`#with_lock` takes
|
39
|
-
|
40
|
-
* `timeout`: The amount of time until a lock expires, in seconds. Defaults to `5`.
|
41
|
-
* `wait`: If a lock exists on the document, wait until that lock expires and try again. Defaults to `false`.
|
38
|
+
`#with_lock` takes an optional [handful of options around retrying](http://rdoc.info/github/afeld/mongoid-locker/Mongoid/Locker:with_lock), so make sure to take a look.
|
42
39
|
|
43
40
|
The default timeout can also be set on a per-class basis:
|
44
41
|
|
@@ -52,15 +49,3 @@ end
|
|
52
49
|
Note that these locks are only enforced when using `#with_lock`, not at the database level. It is useful for transactional operations, where you can make atomic modification of the document with checks. For exmple, you could deduct a purchase from a user's balance... _unless_ they are broke.
|
53
50
|
|
54
51
|
More in-depth method documentation can be found at [rdoc.info](http://rdoc.info/github/afeld/mongoid-locker/frames). Enjoy!
|
55
|
-
|
56
|
-
## Contributing
|
57
|
-
|
58
|
-
Pull requests are welcome. To run tests:
|
59
|
-
|
60
|
-
$ bundle install
|
61
|
-
$ rake
|
62
|
-
|
63
|
-
To auto-run tests as you code:
|
64
|
-
|
65
|
-
$ bundle install
|
66
|
-
$ guard
|
data/Rakefile
CHANGED
@@ -3,18 +3,17 @@
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'rake'
|
6
|
-
require 'appraisal'
|
7
6
|
|
8
7
|
require 'jeweler'
|
9
8
|
Jeweler::Tasks.new do |gem|
|
10
9
|
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
11
|
-
gem.name =
|
12
|
-
gem.homepage =
|
13
|
-
gem.license =
|
14
|
-
gem.summary =
|
15
|
-
gem.description =
|
16
|
-
gem.email =
|
17
|
-
gem.authors = [
|
10
|
+
gem.name = 'mongoid-locker'
|
11
|
+
gem.homepage = 'http://github.com/afeld/mongoid-locker'
|
12
|
+
gem.license = 'MIT'
|
13
|
+
gem.summary = 'Document-level locking for MongoDB via Mongoid'
|
14
|
+
gem.description = 'Allows multiple processes to operate on individual documents in MongoDB while ensuring that only one can act at a time.'
|
15
|
+
gem.email = 'aidan.feldman@gmail.com'
|
16
|
+
gem.authors = ['Aidan Feldman']
|
18
17
|
gem.files.exclude 'demo'
|
19
18
|
# dependencies defined in Gemfile
|
20
19
|
end
|
@@ -26,4 +25,7 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
26
25
|
spec.pattern = FileList['spec/**/*_spec.rb']
|
27
26
|
end
|
28
27
|
|
29
|
-
|
28
|
+
require 'rubocop/rake_task'
|
29
|
+
RuboCop::RakeTask.new(:rubocop)
|
30
|
+
|
31
|
+
task default: [:rubocop, :spec]
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/lib/mongoid/locker.rb
CHANGED
@@ -2,6 +2,9 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'locker', 'wrapper'))
|
|
2
2
|
|
3
3
|
module Mongoid
|
4
4
|
module Locker
|
5
|
+
# Error thrown if document could not be successfully locked.
|
6
|
+
class LockError < Exception; end
|
7
|
+
|
5
8
|
module ClassMethods
|
6
9
|
# A scope to retrieve all locked documents in the collection.
|
7
10
|
#
|
@@ -14,14 +17,14 @@ module Mongoid
|
|
14
17
|
#
|
15
18
|
# @return [Mongoid::Criteria]
|
16
19
|
def unlocked
|
17
|
-
any_of({:
|
20
|
+
any_of({ locked_until: nil }, { :locked_until.lte => Time.now })
|
18
21
|
end
|
19
22
|
|
20
23
|
# Set the default lock timeout for this class. Note this only applies to new locks. Defaults to five seconds.
|
21
24
|
#
|
22
25
|
# @param [Fixnum] new_time the default number of seconds until a lock is considered "expired", in seconds
|
23
26
|
# @return [void]
|
24
|
-
def timeout_lock_after
|
27
|
+
def timeout_lock_after(new_time)
|
25
28
|
@lock_timeout = new_time
|
26
29
|
end
|
27
30
|
|
@@ -35,50 +38,54 @@ module Mongoid
|
|
35
38
|
end
|
36
39
|
|
37
40
|
# @api private
|
38
|
-
def self.included
|
41
|
+
def self.included(mod)
|
39
42
|
mod.extend ClassMethods
|
40
43
|
|
41
|
-
mod.field :locked_at, :
|
42
|
-
mod.field :locked_until, :
|
44
|
+
mod.field :locked_at, type: Time
|
45
|
+
mod.field :locked_until, type: Time
|
43
46
|
end
|
44
47
|
|
45
|
-
|
46
48
|
# Returns whether the document is currently locked or not.
|
47
49
|
#
|
48
50
|
# @return [Boolean] true if locked, false otherwise
|
49
51
|
def locked?
|
50
|
-
!!(
|
52
|
+
!!(locked_until && locked_until > Time.now)
|
51
53
|
end
|
52
54
|
|
53
55
|
# Returns whether the current instance has the lock or not.
|
54
56
|
#
|
55
57
|
# @return [Boolean] true if locked, false otherwise
|
56
58
|
def has_lock?
|
57
|
-
@has_lock && self.locked?
|
59
|
+
!!(@has_lock && self.locked?)
|
58
60
|
end
|
59
61
|
|
60
62
|
# Primary method of plugin: execute the provided code once the document has been successfully locked.
|
61
63
|
#
|
62
64
|
# @param [Hash] opts for the locking mechanism
|
63
65
|
# @option opts [Fixnum] :timeout The number of seconds until the lock is considered "expired" - defaults to the {ClassMethods#lock_timeout}
|
64
|
-
# @option opts [
|
66
|
+
# @option opts [Fixnum] :retries If the document is currently locked, the number of times to retry. Defaults to 0 (note: setting this to 1 is the equivalent of using :wait => true)
|
67
|
+
# @option opts [Float] :retry_sleep How long to sleep between attempts to acquire lock - defaults to time left until lock is available
|
68
|
+
# @option opts [Boolean] :wait If the document is currently locked, wait until the lock expires and try again - defaults to false. If set, :retries will be ignored
|
69
|
+
# @option opts [Boolean] :reload After acquiring the lock, reload the document - defaults to true
|
65
70
|
# @return [void]
|
66
|
-
def with_lock
|
67
|
-
|
68
|
-
|
69
|
-
|
71
|
+
def with_lock(opts = {})
|
72
|
+
have_lock = self.has_lock?
|
73
|
+
|
74
|
+
unless have_lock
|
75
|
+
opts[:retries] = 1 if opts[:wait]
|
76
|
+
lock(opts)
|
77
|
+
end
|
70
78
|
|
71
79
|
begin
|
72
80
|
yield
|
73
81
|
ensure
|
74
|
-
|
82
|
+
unlock unless have_lock
|
75
83
|
end
|
76
84
|
end
|
77
85
|
|
78
|
-
|
79
86
|
protected
|
80
87
|
|
81
|
-
def
|
88
|
+
def acquire_lock(opts = {})
|
82
89
|
time = Time.now
|
83
90
|
timeout = opts[:timeout] || self.class.lock_timeout
|
84
91
|
expiration = time + timeout
|
@@ -87,45 +94,54 @@ module Mongoid
|
|
87
94
|
locked = Mongoid::Locker::Wrapper.update(
|
88
95
|
self.class,
|
89
96
|
{
|
90
|
-
:_id =>
|
97
|
+
:_id => id,
|
91
98
|
'$or' => [
|
92
99
|
# not locked
|
93
|
-
{:
|
100
|
+
{ locked_until: nil },
|
94
101
|
# expired
|
95
|
-
{:
|
102
|
+
{ locked_until: { '$lte' => time } }
|
96
103
|
]
|
97
104
|
},
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
}
|
105
|
+
|
106
|
+
'$set' => {
|
107
|
+
locked_at: time,
|
108
|
+
locked_until: expiration
|
103
109
|
}
|
110
|
+
|
104
111
|
)
|
105
112
|
|
106
113
|
if locked
|
107
114
|
# document successfully updated, meaning it was locked
|
108
115
|
self.locked_at = time
|
109
116
|
self.locked_until = expiration
|
117
|
+
reload unless opts[:reload] == false
|
110
118
|
@has_lock = true
|
111
119
|
else
|
112
|
-
|
120
|
+
@has_lock = false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def lock(opts = {})
|
125
|
+
opts = { retries: 0 }.merge(opts)
|
126
|
+
|
127
|
+
attempts_left = opts[:retries] + 1
|
128
|
+
retry_sleep = opts[:retry_sleep]
|
129
|
+
|
130
|
+
loop do
|
131
|
+
return if acquire_lock(opts)
|
113
132
|
|
114
|
-
|
115
|
-
# doc is locked - wait until it expires
|
116
|
-
wait_time = locked_until - Time.now
|
117
|
-
sleep wait_time if wait_time > 0
|
133
|
+
attempts_left -= 1
|
118
134
|
|
119
|
-
|
120
|
-
|
121
|
-
opts
|
135
|
+
if attempts_left > 0
|
136
|
+
# if not passed a retry_sleep value, we sleep for the remaining life of the lock
|
137
|
+
unless opts[:retry_sleep]
|
138
|
+
locked_until = Mongoid::Locker::Wrapper.locked_until(self)
|
139
|
+
retry_sleep = locked_until - Time.now
|
140
|
+
end
|
122
141
|
|
123
|
-
|
124
|
-
self.reload
|
125
|
-
# retry lock grab
|
126
|
-
self.lock opts
|
142
|
+
sleep retry_sleep if retry_sleep > 0
|
127
143
|
else
|
128
|
-
|
144
|
+
fail LockError.new('could not get lock')
|
129
145
|
end
|
130
146
|
end
|
131
147
|
end
|
@@ -134,13 +150,13 @@ module Mongoid
|
|
134
150
|
# unlock the document in the DB without persisting entire doc
|
135
151
|
Mongoid::Locker::Wrapper.update(
|
136
152
|
self.class,
|
137
|
-
{:
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
}
|
153
|
+
{ _id: id },
|
154
|
+
|
155
|
+
'$set' => {
|
156
|
+
locked_at: nil,
|
157
|
+
locked_until: nil
|
143
158
|
}
|
159
|
+
|
144
160
|
)
|
145
161
|
|
146
162
|
self.locked_at = nil
|
@@ -148,7 +164,4 @@ module Mongoid
|
|
148
164
|
@has_lock = false
|
149
165
|
end
|
150
166
|
end
|
151
|
-
|
152
|
-
# Error thrown if document could not be successfully locked.
|
153
|
-
class LockError < Exception; end
|
154
167
|
end
|