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 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
- - jruby-18mode # JRuby in 1.8 mode
7
- - jruby-19mode # JRuby in 1.9 mode
8
- - rbx-18mode
9
- - rbx-19mode
5
+ - 2.1.2
6
+ - jruby-19mode
7
+ - rbx-2
8
+
10
9
  services: mongodb
11
- gemfile:
12
- - gemfiles/mongoid2.gemfile
13
- - gemfiles/mongoid3.gemfile
14
- matrix:
15
- exclude:
16
- # not supported by Mongoid 3
17
- - rvm: 1.8.7
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
- ## HEAD ([diff](https://github.com/afeld/mongoid-locker/compare/v0.2.1...master?w=1))
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
- gem 'mongoid', '>= 2.4', '<= 3.1'
4
-
5
-
6
- # groups need to be copied to gemfiles/*.gemfile
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', '~> 2.8'
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
- # v0.4.1 doesn't support multiple group names
21
- gem 'appraisal', :git => 'git://github.com/thoughtbot/appraisal.git', :ref => 'ad2aeb99649f6a78f78be5009fb50306f06eaa9f'
26
+ gem 'rubocop', '0.24.0'
22
27
  end
data/Guardfile CHANGED
@@ -1,6 +1,6 @@
1
1
  # More info at https://github.com/guard/guard#readme
2
2
 
3
- guard 'rspec', :version => 2 do
3
+ guard 'rspec' do
4
4
  watch(%r{^spec/.+_spec\.rb$})
5
5
 
6
6
  watch('Gemfile') { "spec" }
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # mongoid-locker [![Build Status](https://secure.travis-ci.org/afeld/mongoid-locker.png?branch=master)](http://travis-ci.org/afeld/mongoid-locker)
1
+ # mongoid-locker [![Build Status](https://secure.travis-ci.org/afeld/mongoid-locker.png?branch=master)](http://travis-ci.org/afeld/mongoid-locker) [![Code Climate](https://codeclimate.com/github/afeld/mongoid-locker.png)](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.8.7, 1.9.2 and 1.9.3, Rubinius 1.8 and 1.9, and JRuby 1.8 and 1.9 with Mongoid 2 and 3 ([where supported](http://travis-ci.org/#!/afeld/mongoid-locker)).
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 a couple options as a hash:
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 = "mongoid-locker"
12
- gem.homepage = "http://github.com/afeld/mongoid-locker"
13
- gem.license = "MIT"
14
- gem.summary = "Document-level locking for MongoDB via Mongoid"
15
- gem.description = %Q{Allows multiple processes to operate on individual documents in MongoDB while ensuring that only one can act at a time.}
16
- gem.email = "aidan.feldman@gmail.com"
17
- gem.authors = ["Aidan Feldman"]
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
- task :default => :spec
28
+ require 'rubocop/rake_task'
29
+ RuboCop::RakeTask.new(:rubocop)
30
+
31
+ task default: [:rubocop, :spec]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.3.0
@@ -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({:locked_until => nil}, {:locked_until.lte => Time.now})
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 new_time
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 mod
41
+ def self.included(mod)
39
42
  mod.extend ClassMethods
40
43
 
41
- mod.field :locked_at, :type => Time
42
- mod.field :locked_until, :type => Time
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
- !!(self.locked_until && self.locked_until > Time.now)
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 [Boolean] :wait If the document is currently locked, wait until the lock expires and try again
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 opts={}, &block
67
- # don't try to re-lock/unlock on recursive calls
68
- had_lock = self.has_lock?
69
- self.lock(opts) unless had_lock
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
- self.unlock unless had_lock
82
+ unlock unless have_lock
75
83
  end
76
84
  end
77
85
 
78
-
79
86
  protected
80
87
 
81
- def lock opts={}
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 => self.id,
97
+ :_id => id,
91
98
  '$or' => [
92
99
  # not locked
93
- {:locked_until => nil},
100
+ { locked_until: nil },
94
101
  # expired
95
- {:locked_until => {'$lte' => time}}
102
+ { locked_until: { '$lte' => time } }
96
103
  ]
97
104
  },
98
- {
99
- '$set' => {
100
- :locked_at => time,
101
- :locked_until => expiration
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
- # couldn't grab lock
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
- if opts[:wait] && locked_until = Mongoid::Locker::Wrapper.locked_until(self)
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
- # only wait once
120
- opts.dup
121
- opts.delete :wait
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
- # reload to update with any new values
124
- self.reload
125
- # retry lock grab
126
- self.lock opts
142
+ sleep retry_sleep if retry_sleep > 0
127
143
  else
128
- raise LockError.new("could not get lock")
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
- {:_id => self.id},
138
- {
139
- '$set' => {
140
- :locked_at => nil,
141
- :locked_until => nil,
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