class MotherBrain::ChefMutex

Allows for motherbrain clients to lock a chef resource. A mutex is created with a type and name. Sending lock to the mutex will then store a data bag item with mutex, the requestor’s client_name, and the current time. An attempt to lock an already-locked mutex will fail if the lock is owned by someone else, or succeed if the lock is owned by the current user.

@example Creating a mutex and obtaining a lock

mutex = ChefMutex.new(chef_environment: "my_environment")

mutex.lock # => true
# do stuff
mutex.unlock # => true

@example Running a block within an obtained lock

mutex = ChefMutex.new(chef_environment: "my_environment")

mutex.synchronize do
  # do stuff
end

Constants

DATA_BAG
LOCK_TYPES

Attributes

force[R]
job[R]
name[R]
report_job_status[R]
type[R]
unlock_on_failure[R]

Public Class Methods

new(options = {}) click to toggle source

@option options [#to_s] :chef_environment

The name of the environment to lock

@option options [Boolean] :force (false)

Force the lock to be written, even if it already exists.

@option options [MotherBrain::Job] :job

A job that will receive status updates during lock/unlock

@option options [Boolean] :report_job_status (false) @option options [Boolean] :unlock_on_failure (true)

If false and the block raises an error, the lock will persist.
# File lib/mb/chef_mutex.rb, line 72
def initialize(options = {})
  options = options.reverse_merge(
    force: false,
    unlock_on_failure: true
  )

  type, name = options.find { |key, value| LOCK_TYPES.include? key }

  @type              = type
  @name              = name
  @force             = options[:force]
  @job               = options[:job]
  @report_job_status = options[:report_job_status]
  @unlock_on_failure = options[:unlock_on_failure]

  lock_manager.register(Actor.current)
end
synchronize(options, &block) click to toggle source

Create a new ChefMutex and run the given block of code within it. Terminate the ChefMutex after the block of code finishes executing.

@see {ChefMutex#initialize}, {ChefMutex#synchronize}

# File lib/mb/chef_mutex.rb, line 30
def synchronize(options, &block)
  mutex = new(options)
  mutex.synchronize(&block)
ensure
  mutex.terminate
end

Public Instance Methods

data_bag_id() click to toggle source

@return [String]

# File lib/mb/chef_mutex.rb, line 91
def data_bag_id
  result = to_s.dup

  result.downcase!
  result.gsub! /[^\w]+/, "-" # dasherize
  result.gsub! /^-+|-+$/, "" # remove dashes from beginning/end

  result
end
lock() click to toggle source

Attempts to create a lock. Fails if the lock already exists.

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 109
def lock
  unless type
    raise InvalidLockType, "Must pass a valid lock type (#{LOCK_TYPES})"
  end

  log.info { "Locking #{to_s}" }

  if job
    job.set_status "Locking #{to_s}"
    job.report_running if report_job_status
  end

  report(attempt_lock)
end
locked?() click to toggle source

Returns whether or not the object is locked.

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 127
def locked?
  !!read
end
synchronize() { || ... } click to toggle source

Obtains a lock, runs the block, and releases the lock when the block completes. Raises a ResourceLocked error if the lock was unobtainable. If the block raises an error, the resource is unlocked, unless unlock_on_failure: true is passed in to the option hash.

@raise [MotherBrain::ResourceLocked] if the lock is unobtainable

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 139
def synchronize
  unless lock
    current_lock = read

    err = "Resource #{current_lock['id']} locked by #{current_lock['client_name']}"
    err << " since #{current_lock['time']} (PID #{current_lock['process_id']})"

    raise ResourceLocked.new(err)
  end

  yield

  unlock
rescue => ex
  ex = ex.respond_to?(:cause) ? ex.cause : ex

  unless ex.is_a?(ResourceLocked)
    unlock if unlock_on_failure
  end

  abort(ex)
end
to_s() click to toggle source

@return [String]

# File lib/mb/chef_mutex.rb, line 102
def to_s
  "#{type}:#{name}"
end
unlock() click to toggle source

Attempts to unlock the lock. Fails if the lock doesn’t exist, or if it is held by someone else

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 166
def unlock
  if job
    job.report_running if report_job_status
    job.set_status("Unlocking #{to_s}")
  end

  attempt_unlock

  report(true)
end

Private Instance Methods

attempt_lock() click to toggle source

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 206
def attempt_lock
  unless self.force
    current_lock = read
    return our_lock?(current_lock) if current_lock
  end

  write
end
attempt_unlock() click to toggle source

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 216
def attempt_unlock
  unless self.force
    current_lock = read

    return unless current_lock && our_lock?(current_lock)
  end

  delete
end
client_name() click to toggle source

@return [String]

# File lib/mb/chef_mutex.rb, line 227
def client_name
  Application.ridley.client_name
end
data_bag() click to toggle source

@return [Ridley::ChainLink]

# File lib/mb/chef_mutex.rb, line 232
def data_bag
  Application.ridley.data_bag
end
delete() click to toggle source

Delete the lock from the data bag.

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 239
def delete
  return true unless locks

  result = locks.delete(data_bag_id)

  lock_manager.unregister(Actor.current)

  result
rescue
  lock_manager.register(Actor.current)
end
ensure_data_bag_exists() click to toggle source

Create our data bag if it doesn’t already exist

# File lib/mb/chef_mutex.rb, line 252
def ensure_data_bag_exists
  data_bag.create(name: DATA_BAG) unless locks
end
finalize_callback() click to toggle source
# File lib/mb/chef_mutex.rb, line 179
def finalize_callback
  lock_manager.async.unregister(Actor.current) if lock_manager.alive?
end
locks() click to toggle source

@return [Ridley::DBIChainLink] if the data bag exists @return [nil] if it does not

# File lib/mb/chef_mutex.rb, line 258
def locks
  result = data_bag.find(DATA_BAG)

  return unless result

  result.item
end
our_lock?(current_lock) click to toggle source

Check to see if the passed in lock was created by us

@param [Hash] current_lock the lock data obtained from read @return [Boolean]

# File lib/mb/chef_mutex.rb, line 198
def our_lock?(current_lock)
  return nil unless current_lock
  return false unless current_lock["client_name"] == client_name
  return false unless current_lock["process_id"] == Process.pid
  true
end
read() click to toggle source

Read the lock from the data bag.

@return [Hash] if the lock exists @return [nil] if it does not

# File lib/mb/chef_mutex.rb, line 270
def read
  return unless locks

  result = locks.find(data_bag_id)

  result.to_hash if result
end
report(result) click to toggle source

Reports a job status @param [Object] result @return [Object] result

# File lib/mb/chef_mutex.rb, line 186
def report(result)
  if job && report_job_status
    job.report_boolean(result)
  end

  result
end
unregister_lock() click to toggle source
# File lib/mb/chef_mutex.rb, line 278
def unregister_lock
  lock_manager.unregister(Actor.current)
end
write() click to toggle source

Write the lock to the data bag.

@return [Boolean]

# File lib/mb/chef_mutex.rb, line 285
def write
  ensure_data_bag_exists

  result = locks.new(
    id: data_bag_id,
    type: type,
    name: name,
    client_name: client_name,
    process_id: Process.pid,
    time: Time.now
  ).save

  lock_manager.register(Actor.current)

  result
rescue
  lock_manager.unregister(Actor.current)
end