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.0.1'

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:



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

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)


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

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.



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

def included(klass)
  klass.extend(Forwardable) unless klass.ancestors.include?(Forwardable)

  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.def_delegators(klass, *MODULE_METHODS)
  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



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

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!


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

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)


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

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



237
238
239
# File 'lib/mongoid/locker.rb', line 237

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



226
227
228
# File 'lib/mongoid/locker.rb', line 226

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)



254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/mongoid/locker.rb', line 254

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