Document-level optimistic locking for MongoDB via Mongoid. Mongoid-Locker is an easy way to ensure only one process can perform a certain operation on a document at a time.
NOTE: Since version 2
Mongoid-Locker relies on MongoDB server time and not current client time, and does not perform any time calculation to get lock or release it. The basis of the current version are unique name of locking and time is set by MongoDB.
NOTE: Please refer to 1-x-stable branch for 1.x.x
documentation. See the UPGRADING guide and CHANGELOG for an overview of the changes.
Tested against:
- MRI:
2.3.8
,2.4.7
,2.5.7
,2.6.6
,2.7.1
,3.0.5
,3.1.3
- JRuby
9.1.17.0
,9.2.11.1
- Mongoid:
5
,6
,7
,8
,9
See .github/workflows/test.yml for the latest test matrix.
Add to your Gemfile
:
gem 'mongoid-locker'
and run bundle install
. In the model you wish to lock, include Mongoid::Locker
after Mongoid::Document
. For example:
class User
include Mongoid::Document
include Mongoid::Locker
field :locking_name, type: String
field :locked_at, type: Time
field :age, type: Integer
end
Then, execute any code you like in a block like so:
user.with_lock do
user.age = 17
user.save!
end
The #with_lock
function takes an optional handful of options, so make sure to take a look.
Note that these locks are only enforced when using #with_lock
, not at the database level. It's useful for transactional operations, where you can make atomic modification of the document with checks. For example, you could deduct a purchase from a user's balance ... unless they are broke.
More in-depth method documentation can be found at RubyDoc.info.
By default, Locker uses fields with :locking_name
and :locked_at
names which should be defined in a model.
class User
include Mongoid::Document
include Mongoid::Locker
field :locking_name, type: String
field :locked_at, type: Time
end
Use Mongoid::Locker.configure
to setup parameters which used by Locker for all models where it's included.
Mongoid::Locker.configure do |config|
config.locking_name_field = :global_locking_name
config.locked_at_field = :global_locked_at
end
class User
include Mongoid::Document
include Mongoid::Locker
field :global_locking_name, type: String
field :global_locked_at, type: Time
end
The locker
method in your model accepts options to setup parameters for the model.
class User
include Mongoid::Document
include Mongoid::Locker
field :locker_locking_name, type: String
field :locker_locked_at, type: Time
locker locked_at_field: :locker_locking_name,
locked_at_field: :locker_locked_at
end
parameter | default | options | description |
---|---|---|---|
locking_name_field | :locking_name |
any field name | field where name of locking is storing, must be of type String |
locked_at_field | :locked_at |
any field name | field where it is storing the time of beginning a lock of a document, must be of type Time |
lock_timeout | 5 |
within this time (in seconds) a document is considered as locked | |
locker_write_concern | { w: 1 } |
see MongoDB Write Concern | a write concern only used for lock and unlock operations |
maximum_backoff | 60.0 |
the highest timeout (in seconds) between retires to lock a document, reaching that value #with_lock method raises Mongoid::Locker::Errors::DocumentCouldNotGetLock |
|
backoff_algorithm | :exponential_backoff |
:locked_at_backoff or custom algorithm |
algorithm used for timeout calculating between retries to lock a document |
locking_name_generator | :secure_locking_name |
custom generator | generator used to generate unique name of a lock |
For instances of a class where Mongoid::Locker
is included, all parameters of a class are available for reading.
document.lock_timeout
#=> 5
A method which is defined in Mongoid::Locker
are available in a class where it is included.
Method #with_lock
passes to the methods a document to which apply #with_lock
and a hash of options. The hash may look like this:
{ retries: Infinity, reload: true, attempt: 0, locking_name: "71c1ccd4-72d9-4a83-bbed-adf65803bd5d" }
A custom backoff algorithmoff must return a value more or equal maximum_backoff
value to force #with_lock
quit trying to lock a document, otherwise #with_lock
will be trying to lock a document INFINITY
times.
Mongoid::Locker.configure do |config|
config.backoff_algorithm = :custom_backoff
end
module Mongoid
module Locker
def self.custom_backoff(doc, _opts)
rand > 0.5 ? 5 : doc.maximum_backoff
end
end
end
A custom locking name generator must return a string to secure uniqueness name of locking.
class User
include Mongoid::Document
include Mongoid::Locker
locker locking_name_generator: :custom_locking_name
field :locker_locking_name, type: String
field :locker_locked_at, type: Time
def self.custom_locking_name(_doc, _opts)
SecureRandom.uuid
end
Please see examples in test_examples_spec.rb file.
Copyright (c) 2012-2020 Aidan Feldman & Contributors
MIT License, see LICENSE for more information.