Module: Mongoid::Locker

Defined in:
lib/mongoid/locker.rb,
lib/mongoid/locker/errors.rb,
lib/mongoid/locker/version.rb,
lib/mongoid/locker/wrapper.rb

Defined Under Namespace

Modules: ClassMethods, Errors, Wrapper

Constant Summary collapse

MODULE_METHODS =

Available parameters for Mongoid::Locker module, a class where the module is included and it’s instances.

%i[
  locking_name_field
  locked_at_field
  maximum_backoff
  lock_timeout
  locker_write_concern
  backoff_algorithm
  locking_name_generator
].freeze
VERSION =
'2.2.0'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.configure {|_self| ... } ⇒ Object

Sets configuration using a block.

Examples:

Mongoid::Locker.configure do |config|
  config.locking_name_field     = :locking_name
  config.locked_at_field        = :locked_at
  config.lock_timeout           = 5
  config.locker_write_concern   = { w: 1 }
  config.maximum_backoff        = 60.0
  config.backoff_algorithm      = :exponential_backoff
  config.locking_name_generator = :secure_locking_name
end

Yields:

  • (_self)

Yield Parameters:



85
86
87
# File 'lib/mongoid/locker.rb', line 85

def configure
  yield(self) if block_given?
end

.exponential_backoff(_doc, opts) ⇒ Float

Returns random number of seconds depend on passed options.

Examples:

Mongoid::Locker.exponential_backoff(doc, { attempt: 0 })
#=> 1.2280675023095662
Mongoid::Locker.exponential_backoff(doc, { attempt: 1 })
#=> 2.901641863236713
Mongoid::Locker.exponential_backoff(doc, { attempt: 2 })
#=> 4.375030664612267

Parameters:

  • _doc (Mongoid::Document)
  • opts (Hash)

    (see #with_lock)

Returns:

  • (Float)


47
48
49
# File 'lib/mongoid/locker.rb', line 47

def exponential_backoff(_doc, opts)
  2**opts[:attempt] + rand
end

.included(klass) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/mongoid/locker.rb', line 105

def included(klass)
  klass.extend ClassMethods
  klass.singleton_class.instance_eval { attr_accessor(*MODULE_METHODS) }

  klass.locking_name_field = locking_name_field
  klass.locked_at_field = locked_at_field
  klass.lock_timeout = lock_timeout
  klass.locker_write_concern = locker_write_concern
  klass.maximum_backoff = maximum_backoff
  klass.backoff_algorithm = backoff_algorithm
  klass.locking_name_generator = locking_name_generator

  klass.delegate(*MODULE_METHODS, to: :class)
  klass.singleton_class.delegate(*(methods(false) - MODULE_METHODS.flat_map { |method| [method, "#{method}=".to_sym] } - %i[included reset! configure]), to: self)
end

.locked_at_backoff(doc, opts) ⇒ Float | Integer, 0

Returns time in seconds remaining to complete the lock of the provided document. Makes requests to the database.

Examples:

Mongoid::Locker.locked_at_backoff(doc, opts)
#=> 2.32422359

Parameters:

  • doc (Mongoid::Document)
  • opts (Hash)

    (see #with_lock)

Returns:

  • (Float | Integer)
  • (0)

    if the provided document is not locked



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/mongoid/locker.rb', line 61

def locked_at_backoff(doc, opts)
  return doc.maximum_backoff if opts[:attempt] * doc.lock_timeout >= doc.maximum_backoff

  locked_at = Wrapper.locked_at(doc).to_f
  return 0 unless locked_at > 0

  current_time = Wrapper.current_mongodb_time(doc.class).to_f
  delay = doc.lock_timeout - (current_time - locked_at)

  delay < 0 ? 0 : delay + rand
end

.reset!Object

Resets to default configuration.

Examples:

Mongoid::Locker.reset!


93
94
95
96
97
98
99
100
101
102
# File 'lib/mongoid/locker.rb', line 93

def reset!
  # The parameters used by default.
  self.locking_name_field     = :locking_name
  self.locked_at_field        = :locked_at
  self.lock_timeout           = 5
  self.locker_write_concern   = { w: 1 }
  self.maximum_backoff        = 60.0
  self.backoff_algorithm      = :exponential_backoff
  self.locking_name_generator = :secure_locking_name
end

.secure_locking_name(_doc, opts) ⇒ String

Generates secure random string of name#attempt format.

Examples:

Mongoid::Locker.secure_locking_name(doc, { attempt: 1 })
#=> "zLmulhOy9yn_NE886OWNYw#1"

Parameters:

  • doc (Mongoid::Document)
  • opts (Hash)

    (see #with_lock)

Returns:

  • (String)


30
31
32
# File 'lib/mongoid/locker.rb', line 30

def secure_locking_name(_doc, opts)
  "#{SecureRandom.urlsafe_base64}##{opts[:attempt]}"
end

Instance Method Details

#has_lock?Boolean

Returns whether the current instance has the lock or not.

Examples:

document.has_lock?
#=> false

Returns:

  • (Boolean)

    true if locked, false otherwise



231
232
233
# File 'lib/mongoid/locker.rb', line 231

def has_lock?
  @has_lock || false
end

#locked?Boolean

Returns whether the document is currently locked in the database or not.

Examples:

document.locked?
#=> false

Returns:

  • (Boolean)

    true if locked, false otherwise



220
221
222
# File 'lib/mongoid/locker.rb', line 220

def locked?
  persisted? && self.class.where(_id: id).locked.limit(1).count == 1
end

#with_lock(**opts) ⇒ Object

Executes the provided code once the document has been successfully locked. Otherwise, raises error after the number of retries to lock the document is exhausted or it is reached ClassMethods#maximum_backoff limit (depending what comes first).

Examples:

document.with_lock(reload: true, retries: 3) do
  document.quantity = 17
  document.save!
end

Parameters:

  • opts (Hash)

    for the locking mechanism

Options Hash (**opts):

  • :retries (Fixnum) — default: INFINITY

    If the document is currently locked, the number of times to retry

  • :reload (Boolean) — default: true

    After acquiring the lock, reload the document

  • :attempt (Integer) — default: 0

    Increment with each retry (not accepted by the method)

  • :locking_name (String)

    Generate with each retry (not accepted by the method)



248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/mongoid/locker.rb', line 248

def with_lock(**opts)
  opts = opts.dup
  opts[:retries] ||= Float::INFINITY
  opts[:reload] = opts[:reload] != false

  acquire_lock(opts) if persisted? && (had_lock = !has_lock?)

  begin
    yield
  ensure
    unlock!(opts) if had_lock
  end
end