mongoid-locker 1.0.1 → 2.0.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.
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Locker
5
+ module Errors
6
+ # Default parent Mongoid::Locker error for all custom errors.
7
+ class MongoidLockerError < StandardError
8
+ BASE_KEY = 'mongoid.locker.errors.messages'
9
+
10
+ def initialize(key, **params)
11
+ message = I18n.translate("#{BASE_KEY}.#{key}.message", params)
12
+
13
+ super("\nmessage:\n #{message}")
14
+ end
15
+ end
16
+
17
+ # Raised when a document could not be successfully locked in the database.
18
+ class DocumentCouldNotGetLock < MongoidLockerError
19
+ KEY = 'document_could_not_get_lock'
20
+
21
+ # @example Create new error.
22
+ # DocumentCouldNotGetLock.new(Account, '1234')
23
+ #
24
+ # @param klass [Class] the model class
25
+ # @param id [String, BSON::ObjectId] the document's id
26
+ def initialize(klass, id)
27
+ super(KEY, klass: klass, id: id.to_s)
28
+ end
29
+ end
30
+
31
+ # Raised when trying to pass an invalid parameter to locker method by a class.
32
+ class InvalidParameter < MongoidLockerError
33
+ KEY = 'invalid_parameter'
34
+
35
+ # @example Create new error.
36
+ # InvalidParameter.new(User, :lock_timeout)
37
+ #
38
+ # @param klass [Class] the model class
39
+ # @param parameter [String, Symbol] the class parameter
40
+ def initialize(klass, parameter)
41
+ super(KEY, klass: klass, parameter: parameter)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mongoid
2
4
  module Locker
3
- VERSION = '1.0.1'.freeze
5
+ VERSION = '2.0.0'
4
6
  end
5
7
  end
@@ -1,3 +1,121 @@
1
- raise "Incompatible Mongoid #{version} version" unless %w[4 5 6 7].include?(version = Mongoid::VERSION.split('.')[0])
1
+ # frozen_string_literal: true
2
2
 
3
- require "mongoid/locker/wrapper#{version}"
3
+ module Mongoid
4
+ module Locker
5
+ # Set of methods to interact with the database.
6
+ module Wrapper
7
+ # Finds provided document with provided options, locks (sets +locking_name_field+ and +locked_at_field+ fields), and returns the fields.
8
+ #
9
+ # @example
10
+ # Mongoid::Locker::Wrapper.find_and_lock(document, opts)
11
+ # #=> {"locked_at"=>2019-03-19 07:51:24 UTC, "locking_name"=>nil}
12
+ #
13
+ # @example
14
+ # Mongoid::Locker::Wrapper.find_and_lock(document, opts)
15
+ # # => nil
16
+ #
17
+ # @param doc [Mongoid::Document]
18
+ # @param opts [Hash] (see #with_lock)
19
+ # @return [Hash] with +locking_name_field+ and +locked_at_field+ fields
20
+ # @return [nil] if the document was not found, was already locked
21
+ def self.find_and_lock(doc, opts)
22
+ model = doc.class
23
+ filter = {
24
+ _id: doc.id,
25
+ '$or': [
26
+ {
27
+ '$or': [
28
+ { model.locking_name_field => { '$exists': false } },
29
+ { model.locked_at_field => { '$exists': false } }
30
+ ]
31
+ },
32
+ {
33
+ '$or': [
34
+ { model.locking_name_field => { '$eq': nil } },
35
+ { model.locked_at_field => { '$eq': nil } }
36
+ ]
37
+ },
38
+ {
39
+ '$where': "new Date() - this.#{model.locked_at_field} >= #{model.lock_timeout * 1000}"
40
+ }
41
+ ]
42
+ }
43
+ update = {
44
+ '$set': { model.locking_name_field => opts[:locking_name] },
45
+ '$currentDate': { model.locked_at_field => true }
46
+ }
47
+ options = {
48
+ return_document: :after,
49
+ projection: { _id: false, model.locking_name_field => true, model.locked_at_field => true },
50
+ write_concern: model.locker_write_concern
51
+ }
52
+
53
+ model.collection.find_one_and_update(filter, update, options)
54
+ end
55
+
56
+ # Finds provided document with provided options, unlocks (sets +locking_name_field+ and +locked_at_field+ fields to +nil+).
57
+ #
58
+ # @example
59
+ # Mongoid::Locker::Wrapper.find_and_unlock(doc, opts)
60
+ # #=> true
61
+ # Mongoid::Locker::Wrapper.find_and_unlock(doc, opts)
62
+ # #=> false
63
+ #
64
+ # @param doc [Mongoid::Document]
65
+ # @param opts [Hash] (see #with_lock)
66
+ # @return [Boolean]
67
+ # @return [true] if the document was unlocked
68
+ # @return [false] if the document was not found, was not unlocked
69
+ def self.find_and_unlock(doc, opts)
70
+ model = doc.class
71
+ filter = {
72
+ _id: doc.id,
73
+ model.locking_name_field => opts[:locking_name]
74
+ }
75
+ update = {
76
+ '$set': {
77
+ model.locking_name_field => nil,
78
+ model.locked_at_field => nil
79
+ }
80
+ }
81
+ options = { write_concern: model.locker_write_concern }
82
+
83
+ result = model.collection.update_one(filter, update, options)
84
+ result.ok? && result.written_count == 1
85
+ end
86
+
87
+ # Returns value of +locked_at_field+ field for provided document.
88
+ #
89
+ # @example
90
+ # Mongoid::Locker::Wrapper.locked_at(document)
91
+ # #=> 2019-06-03 13:50:46 UTC
92
+ #
93
+ # @param doc [Mongoid::Document]
94
+ # @return [Time] +locked_at_field+ field time
95
+ # @return [nil] if response was failed
96
+ def self.locked_at(doc)
97
+ result = doc.class.collection.find(
98
+ { _id: doc.id },
99
+ projection: { _id: false, doc.locked_at_field => true },
100
+ limit: 1
101
+ ).first
102
+
103
+ result[doc.locked_at_field.to_s] if result
104
+ end
105
+
106
+ # Returns the local database server time in UTC.
107
+ #
108
+ # @example
109
+ # Mongoid::Locker::Wrapper.current_mongodb_time(User)
110
+ # #=> 2019-03-19 07:24:36 UTC
111
+ #
112
+ # @param model [Class] the model class
113
+ # @return [Time] current time
114
+ # @return [nil] if response was failed
115
+ def self.current_mongodb_time(model)
116
+ info = model.collection.database.command(isMaster: 1)
117
+ info.ok? ? info.documents.first['localTime'] : nil
118
+ end
119
+ end
120
+ end
121
+ end
@@ -1,20 +1,22 @@
1
- lib = File.expand_path('lib', __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+
3
5
  require 'mongoid/locker/version'
4
6
 
5
- Gem::Specification.new do |spec|
6
- spec.name = 'mongoid-locker'
7
- spec.version = Mongoid::Locker::VERSION
8
- spec.authors = ['Aidan Feldman']
9
- spec.email = ['aidan.feldman@gmail.com']
7
+ Gem::Specification.new do |s|
8
+ s.name = 'mongoid-locker'
9
+ s.version = Mongoid::Locker::VERSION
10
+ s.authors = ['Aidan Feldman']
11
+ s.email = ['aidan.feldman@gmail.com']
10
12
 
11
- spec.summary = 'Document-level locking for MongoDB via Mongoid.'
12
- spec.description = 'Allows multiple processes to operate on individual documents in MongoDB while ensuring that only one can act at a time.'
13
- spec.homepage = 'https://github.com/mongoid/mongoid-locker'
14
- spec.license = 'MIT'
13
+ s.summary = 'Document-level optimistic locking for MongoDB via Mongoid.'
14
+ s.description = 'Allows multiple processes to operate on individual documents in MongoDB while ensuring that only one can act at a time.'
15
+ s.homepage = 'https://github.com/mongoid/mongoid-locker'
16
+ s.license = 'MIT'
15
17
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|demo)/}) }
17
- spec.require_paths = ['lib']
18
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
19
+ s.require_paths = ['lib']
18
20
 
19
- spec.add_runtime_dependency 'mongoid', '>= 4.0', '< 8'
21
+ s.add_dependency 'mongoid', '>= 5.0', '< 8'
20
22
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid-locker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aidan Feldman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-23 00:00:00.000000000 Z
11
+ date: 2019-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mongoid
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.0'
19
+ version: '5.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '8'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '4.0'
29
+ version: '5.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '8'
@@ -38,7 +38,6 @@ executables: []
38
38
  extensions: []
39
39
  extra_rdoc_files: []
40
40
  files:
41
- - ".document"
42
41
  - ".gitignore"
43
42
  - ".rspec"
44
43
  - ".rubocop.yml"
@@ -54,14 +53,12 @@ files:
54
53
  - RELEASING.md
55
54
  - Rakefile
56
55
  - UPGRADING.md
56
+ - lib/config/locales/en.yml
57
57
  - lib/mongoid-locker.rb
58
58
  - lib/mongoid/locker.rb
59
+ - lib/mongoid/locker/errors.rb
59
60
  - lib/mongoid/locker/version.rb
60
61
  - lib/mongoid/locker/wrapper.rb
61
- - lib/mongoid/locker/wrapper4.rb
62
- - lib/mongoid/locker/wrapper5.rb
63
- - lib/mongoid/locker/wrapper6.rb
64
- - lib/mongoid/locker/wrapper7.rb
65
62
  - mongoid-locker.gemspec
66
63
  homepage: https://github.com/mongoid/mongoid-locker
67
64
  licenses:
@@ -85,5 +82,5 @@ requirements: []
85
82
  rubygems_version: 3.0.3
86
83
  signing_key:
87
84
  specification_version: 4
88
- summary: Document-level locking for MongoDB via Mongoid.
85
+ summary: Document-level optimistic locking for MongoDB via Mongoid.
89
86
  test_files: []
data/.document DELETED
@@ -1,5 +0,0 @@
1
- lib/**/*.rb
2
- bin/*
3
- -
4
- features/**/*.feature
5
- LICENSE.txt
@@ -1,22 +0,0 @@
1
- module Mongoid
2
- module Locker
3
- # Normalizes queries between various Mongoid versions.
4
- module Wrapper
5
- # Update the document for the provided Class matching the provided query with the provided setter.
6
- #
7
- # @param [Class] The model class
8
- # @param [Hash] The Mongoid query
9
- # @param [Hash] The Mongoid setter
10
- # @return [Boolean] true if the document was successfully updated, false otherwise
11
- def self.update(klass, query, setter)
12
- klass.with(safe: true).collection.find(query).update(setter)['n'] == 1
13
- end
14
-
15
- def self.locked_until(doc)
16
- existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
17
- existing = doc.class.where(existing_query).limit(1).only(doc.locked_until_field).first
18
- existing ? existing[doc.locked_until_field] : nil
19
- end
20
- end
21
- end
22
- end
@@ -1,27 +0,0 @@
1
- module Mongoid
2
- module Locker
3
- # Normalizes queries between various Mongoid versions.
4
- module Wrapper
5
- # Update the document for the provided Class matching the provided query with the provided setter.
6
- #
7
- # @param [Class] The model class
8
- # @param [Hash] The Mongoid query
9
- # @param [Hash] The Mongoid setter
10
- # @return [Boolean] true if the document was successfully updated, false otherwise
11
- def self.update(klass, query, setter)
12
- rc = klass.with(write: { w: 1 }).collection.find(query).update_one(setter)
13
- rc.ok? && rc.documents.first['n'] == 1
14
- end
15
-
16
- # Determine whether the provided document is locked in the database or not.
17
- #
18
- # @param [Class] The model instance
19
- # @return [Time] The timestamp of when the document is locked until, nil if not locked.
20
- def self.locked_until(doc)
21
- existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
22
- existing = doc.class.where(existing_query).limit(1).only(doc.locked_until_field).first
23
- existing ? existing[doc.locked_until_field] : nil
24
- end
25
- end
26
- end
27
- end
@@ -1,29 +0,0 @@
1
- module Mongoid
2
- module Locker
3
- # Normalizes queries between various Mongoid versions.
4
- module Wrapper
5
- # Update the document for the provided Class matching the provided query with the provided setter.
6
- #
7
- # @param [Class] The model class
8
- # @param [Hash] The Mongoid query
9
- # @param [Hash] The Mongoid setter
10
- # @return [Boolean] true if the document was successfully updated, false otherwise
11
- def self.update(klass, query, setter)
12
- rc = klass.with(write: { w: 1 }) do
13
- klass.collection.find(query).update_one(setter)
14
- end
15
- rc.ok? && rc.documents.first['n'] == 1
16
- end
17
-
18
- # Determine whether the provided document is locked in the database or not.
19
- #
20
- # @param [Class] The model instance
21
- # @return [Time] The timestamp of when the document is locked until, nil if not locked.
22
- def self.locked_until(doc)
23
- existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
24
- existing = doc.class.where(existing_query).limit(1).only(doc.locked_until_field).first
25
- existing ? existing[doc.locked_until_field] : nil
26
- end
27
- end
28
- end
29
- end
@@ -1,2 +0,0 @@
1
- # same with wrapper6
2
- require 'mongoid/locker/wrapper6'