mongoid-locker 0.2.1 → 0.3.0

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