Class: Furnish::Protocol

Inherits:
Object
  • Object
show all
Defined in:
lib/furnish/protocol.rb

Overview

Furnish::Protocol implements a validating protocol for state transitions. It is an optional feature and not necessary for using Furnish.

A Furnish::ProvisionerGroup looks like this:

thing_a -> thing_b -> thing_c

Where these things are furnish provisioners. When the scheduler says it’s ready to provision this group, it executes thing_a’s startup routine, passes its results to thing_b’s startup routine, and then thing_b’s startup routine passes its things to thing_c’s startup routine.

Presuming this all succeeds (and returns truthy values), the group is marked as ‘solved’, the scheduler considers it finished and will ignore future requests to provision it again. It also will start working on anything that depends on its solved state.

A problem is that you have to know ahead of time how a, b, and c interact for this to be successful. For example, you can’t allocate an EC2 security group, then an instance, then a VPC, and expect the security group and instance to live in that VPC. It’s not only out of order, but the security group doesn’t know enough at the time it runs to leverage the VPC, because the VPC doesn’t exist yet.

Furnish::Protocol lets you describe what each provisioner requires, what it accepts, and what it yields, so that analysis can be performed at scheduler time (when it’s configured) instead of provisioning time (when it actually runs). This surfaces issues quicker and has some additional advantages for interfaces where users may not have full visibility into what the provisioners do (such as closed source provisioners, or inadequately documented ones).

Here’s a description of how this logic works for two adjacent provisioners in the group, a and b:

  • if Provisioner A and Provisioner B implement Furnish::Protocol

    • if B requires anything, and A yields all of it with the proper types

      • if B accepts anything, and A yields any of it with the proper types

        • success

      • else failure

    • if B accepts anything, and A yields any of it with the proper types

      • success

    • if B has #accepts_from_any set to true

      • success

    • if B accepts nothing

      • success

    • else failure

  • else success

Provisioners at the head and tail do not get subject to acceptance tests because there’s nothing to yield, or nothing to accept what is yielded.

Constant Summary collapse

VALIDATOR_NAMES =

:method: :call-seq:

yields(name)
yields(name, description)
yields(name, description, type)

Specifies what the provisioner is expected to yield. This is the producer for the consumer counterparts #requires and #accepts. The configuration made here will be used in both #requires_from and #accepts_from for determining if two provisioners can talk to each other.

See the logic explanation in Furnish::Protocol for more information.

See Furnish::Provisioner::API.configure_startup for a usage example.

[:requires, :accepts, :yields]

Instance Method Summary collapse

Constructor Details

#initializeProtocol

Construct a Furnish::Protocol object.



112
113
114
115
# File 'lib/furnish/protocol.rb', line 112

def initialize
  @hash = Hash[VALIDATOR_NAMES.map { |n| [n, { }] }]
  @configuring = false
end

Instance Method Details

#[](key) ⇒ Object

look up a rule set – generally should not be used by consumers.



152
153
154
# File 'lib/furnish/protocol.rb', line 152

def [](key)
  @hash[key]
end

#accepts_from(protocol) ⇒ Object

For a passed Furnish::Protocol object, ensures that at least one thing this protocol object accepts is satisfied by what that Furnish::Protocol object yields.

See the logic discussion in Furnish::Protocol for a deeper explanation.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/furnish/protocol.rb', line 184

def accepts_from(protocol)
  not_configurable(__method__)

  return true unless protocol

  yp = protocol[:yields]
  ap = self[:accepts]

  return true if ap.keys.empty?

  if (yp.keys & ap.keys).empty?
    return self[:accepts_from_any]
  end

  return ap.keys.any? { |k| yp.has_key?(k) && ap[k][:type].ancestors.include?(yp[k][:type]) }
end

#accepts_from_any(val) ⇒ Object

Allow #accepts_from to completely mismatch with yields from a compared provisioner and still succeed. Use with caution.

See the logic discussion in Furnish::Protocol for a deeper explanation.



138
139
140
# File 'lib/furnish/protocol.rb', line 138

def accepts_from_any(val)
  @hash[:accepts_from_any] = val
end

#accepts_from_any?Boolean

Predicate for the value of #accepts_from_any.

Returns:

  • (Boolean)


145
146
147
# File 'lib/furnish/protocol.rb', line 145

def accepts_from_any?
  !!@hash[:accepts_from_any]
end

#configure(&block) ⇒ Object

This runs the block given instance evaled against the current Furnish::Protocol object. It is used by Furnish::Provisioner::API’s syntax sugar.

Additionally it sets a simple lock to ensure the assertions Furnish::Protocol provides cannot be used during configuration time, like #accept_from and #requires_from.



126
127
128
129
130
# File 'lib/furnish/protocol.rb', line 126

def configure(&block)
  @configuring = true
  instance_eval(&block)
  @configuring = false
end

#requires_from(protocol) ⇒ Object

For a passed Furnish::Protocol object, ensures that this protocol object satisfies its requirements based on what it yields.

See the logic discussion in Furnish::Protocol for a deeper explanation.



162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/furnish/protocol.rb', line 162

def requires_from(protocol)
  not_configurable(__method__)

  return true unless protocol

  yp = protocol[:yields]
  rp = self[:requires]

  rp.keys.empty? ||
    (
      (yp.keys & rp.keys).sort == rp.keys.sort &&
      rp.keys.all? { |k| rp[k][:type].ancestors.include?(yp[k][:type]) }
    )
end