1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
require_relative '../puppet/thread_local'
# Puppet::Context is a system for tracking services and contextual information
# that puppet needs to be able to run. Values are "bound" in a context when it is created
# and cannot be changed; however a child context can be created, using
# {#override}, that provides a different value.
#
# When binding a {Proc}, the proc is called when the value is looked up, and the result
# is memoized for subsequent lookups. This provides a lazy mechanism that can be used to
# delay expensive production of values until they are needed.
#
# @api private
class Puppet::Context
require_relative 'context/trusted_information'
class UndefinedBindingError < Puppet::Error; end
class StackUnderflow < Puppet::Error; end
class UnknownRollbackMarkError < Puppet::Error; end
class DuplicateRollbackMarkError < Puppet::Error; end
# @api private
def initialize(initial_bindings)
@stack = Puppet::ThreadLocal.new(EmptyStack.new.push(initial_bindings))
# By initializing @rollbacks to nil and creating a hash lazily when #mark or
# #rollback are called we ensure that the hashes are never shared between
# threads and it's safe to mutate them
@rollbacks = Puppet::ThreadLocal.new(nil)
end
# @api private
def push(overrides, description = '')
@stack.value = @stack.value.push(overrides, description)
end
# Push a context and make this global across threads
# Do not use in a context where multiple threads may already exist
#
# @api private
def unsafe_push_global(overrides, description = '')
@stack = Puppet::ThreadLocal.new(
@stack.value.push(overrides, description)
)
end
# @api private
def pop
@stack.value = @stack.value.pop
end
# @api private
def lookup(name, &block)
@stack.value.lookup(name, &block)
end
# @api private
def override(bindings, description = '', &block)
saved_point = @stack.value
push(bindings, description)
yield
ensure
@stack.value = saved_point
end
# Mark a place on the context stack to later return to with {rollback}.
#
# @param name [Object] The identifier for the mark
#
# @api private
def mark(name)
@rollbacks.value ||= {}
if @rollbacks.value[name].nil?
@rollbacks.value[name] = @stack.value
else
raise DuplicateRollbackMarkError, _("Mark for '%{name}' already exists") % { name: name }
end
end
# Roll back to a mark set by {mark}.
#
# Rollbacks can only reach a mark accessible via {pop}. If the mark is not on
# the current context stack the behavior of rollback is undefined.
#
# @param name [Object] The identifier for the mark
#
# @api private
def rollback(name)
@rollbacks.value ||= {}
if @rollbacks.value[name].nil?
raise UnknownRollbackMarkError, _("Unknown mark '%{name}'") % { name: name }
end
@stack.value = @rollbacks.value.delete(name)
end
# Base case for Puppet::Context::Stack.
#
# @api private
class EmptyStack
# Lookup a binding. Since there are none in EmptyStack, this always raises
# an exception unless a block is passed, in which case the block is called
# and its return value is used.
#
# @api private
def lookup(name, &block)
if block
block.call
else
raise UndefinedBindingError, _("Unable to lookup '%{name}'") % { name: name }
end
end
# Base case of #pop always raises an error since this is the bottom
#
# @api private
def pop
raise(StackUnderflow,
_('Attempted to pop, but already at root of the context stack.'))
end
# Push bindings onto the stack by creating a new Stack object with `self` as
# the parent
#
# @api private
def push(overrides, description = '')
Puppet::Context::Stack.new(self, overrides, description)
end
# Return the bindings table, which is always empty here
#
# @api private
def bindings
{}
end
end
# Internal implementation of the bindings stack used by Puppet::Context. An
# instance of Puppet::Context::Stack represents one level of bindings. It
# caches a merged copy of all the bindings in the stack up to this point.
# Each element of the stack is immutable, allowing the base to be shared
# between threads.
#
# @api private
class Stack
attr_reader :bindings
def initialize(parent, bindings, description = '')
@parent = parent
@bindings = parent.bindings.merge(bindings || {})
@description = description
end
# Lookup a binding in the current stack. Return the value if it is present.
# If the value is a stored Proc, evaluate, cache, and return the result. If
# no binding is found and a block is passed evaluate it and return the
# result. Otherwise an exception is raised.
#
# @api private
def lookup(name, &block)
if @bindings.include?(name)
value = @bindings[name]
value.is_a?(Proc) ? (@bindings[name] = value.call) : value
elsif block
block.call
else
raise UndefinedBindingError,
_("Unable to lookup '%{name}'") % { name: name }
end
end
# Pop one level off the stack by returning the parent object.
#
# @api private
def pop
@parent
end
# Push bindings onto the stack by creating a new Stack object with `self` as
# the parent
#
# @api private
def push(overrides, description = '')
Puppet::Context::Stack.new(self, overrides, description)
end
end
end
|