Class: MotherBrain::ChefMutex

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Celluloid, Celluloid::Notifications, MB::Mixin::Services, Logging
Defined in:
lib/mb/chef_mutex.rb

Overview

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.

Examples:

Creating a mutex and obtaining a lock


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

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

Running a block within an obtained lock


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

mutex.synchronize do
  # do stuff
end

Constant Summary collapse

DATA_BAG =
"_motherbrain_locks_".freeze
LOCK_TYPES =
[
  :chef_environment
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

add_argument_header, dev, filename, #log_exception, logger, #logger, reset, set_logger, setup

Constructor Details

#initialize(options = {}) ⇒ ChefMutex

Returns a new instance of ChefMutex.

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :chef_environment (#to_s)

    The name of the environment to lock

  • :force (Boolean) — default: false

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

  • :job (MotherBrain::Job)

    A job that will receive status updates during lock/unlock

  • :report_job_status (Boolean) — default: false
  • :unlock_on_failure (Boolean) — default: true

    If false and the block raises an error, the lock will persist.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# 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

Instance Attribute Details

#forceObject (readonly)

Returns the value of attribute force.



54
55
56
# File 'lib/mb/chef_mutex.rb', line 54

def force
  @force
end

#jobObject (readonly)

Returns the value of attribute job.



55
56
57
# File 'lib/mb/chef_mutex.rb', line 55

def job
  @job
end

#nameObject (readonly)

Returns the value of attribute name.



52
53
54
# File 'lib/mb/chef_mutex.rb', line 52

def name
  @name
end

#report_job_statusObject (readonly)

Returns the value of attribute report_job_status.



56
57
58
# File 'lib/mb/chef_mutex.rb', line 56

def report_job_status
  @report_job_status
end

#typeObject (readonly)

Returns the value of attribute type.



51
52
53
# File 'lib/mb/chef_mutex.rb', line 51

def type
  @type
end

#unlock_on_failureObject (readonly)

Returns the value of attribute unlock_on_failure.



57
58
59
# File 'lib/mb/chef_mutex.rb', line 57

def unlock_on_failure
  @unlock_on_failure
end

Class Method Details

.synchronize(options, &block) ⇒ Object

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

See Also:

  • {ChefMutex#synchronize}


30
31
32
33
34
35
# File 'lib/mb/chef_mutex.rb', line 30

def synchronize(options, &block)
  mutex = new(options)
  mutex.synchronize(&block)
ensure
  mutex.terminate
end

Instance Method Details

#data_bag_idString

Returns:

  • (String)


91
92
93
94
95
96
97
98
99
# 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

#lockBoolean

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

Returns:

  • (Boolean)


109
110
111
112
113
114
115
116
117
118
119
120
121
122
# 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?Boolean

Returns whether or not the object is locked.

Returns:

  • (Boolean)


127
128
129
# File 'lib/mb/chef_mutex.rb', line 127

def locked?
  !!read
end

#synchronizeBoolean

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.

Returns:

  • (Boolean)

Raises:



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 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_sString

Returns:

  • (String)


102
103
104
# File 'lib/mb/chef_mutex.rb', line 102

def to_s
  "#{type}:#{name}"
end

#unlockBoolean

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

Returns:

  • (Boolean)


166
167
168
169
170
171
172
173
174
175
# 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