Skip to content
Philip (flip) Kromer edited this page Jun 4, 2012 · 2 revisions

gorillib/record/overlay (WIP -- NOT READY YET) -- Progressively overlayed configuration

Record::Overlay lets you specify an ordered stack of attributes, each overriding the following. For example, you may wih to load your program's configuration using

 (wins) value set directly on self
        commandline parameters
        environment variables
        per-user config file
        machine-wide config file
(loses) application defaults

In the Ironfan toolset, servers are organized into hierarchical groups -- 'provider', then 'cluster', then 'facet'. Its configuration builder lets you describe a cloud server as follows:

 (wins) server-specific
        facet
        cluster
        provider
(loses) default

Higher layers override lower layers

In the following example, we set a value for the environment attribute on the cluster, then override it in the facet:

cluster :gibbon do |c|
  c.environment  :prod

  c.facet :worker do |f|
    # since no value has been set on the facet, a read pokes through to the cluster's value
    f.environment                 #=> :prod
    # setting a value on the facet overrides the value from the cluster:
    f.environment :dev
    f.environment                 #=> :dev
  end
end

This diagram might help, for a hypothetical 'owner' setting in confilgiere:

 Settings:
                               +-- reads come in from the top
 _______________________       |
 | .owner               |     \|/
 +----------------------+------------------+
 | self                 |                  |
 | overlay(:commandline)  |                  |
 | overlay(:env_vars)     |    'bob'         | <-- writes go in from the side
 | overlay(:user_conf)    |                  |
 | overlay(:system_conf)  |    'alice'       | <--
 | default, if any      |                  |
 +----------------------+------------------+

In this diagram, values have been set on the objects at the 'env_vars' and 'system_conf' layers, so when you read the final settings value with Settings.owner it returns "bob". If the user had specified --owner="charlie" on the commandline, it would have set a value in the 'commandline' row and taken precedence; Settings.owner would return "charlie" instead.

Late Resolution means overlays are dynamic

Overlay layers are late-resolved -- they return whatever value is appropriate at time of read:

cluster(:gibbon) do |c|
  c.environment(:prod)
  # since no value has been set on the facet, a read pokes through to the cluster's value
  c.facet(:worker).environment      #=> :prod

  # changing the cluster's value changes the facet's effective value as well:
  c.environment(:test)
  c.facet(:worker).environment      #=> :test
end

Defining an Overlay

You can apply overlay behavior to any Gorillib::Record: the Builder, Model and Bucket classes, or any diabolical little variant you've cooked up.

An Overlay'ed object's read_unset_attribute calls super, so if there is a default value but neither the object nor any overlay have a value set,

Cascading attributes

I want to be able to define what's uniform across a collection at one layer, and customize only what's special at another. In this example, our overlay stack is cluster > facet; each has a volumes collection field. The :master facet overrides the size and tags applied to the volume; in the latter case this means modifying the value read from the cluster layer and applying it to the facet layer:

```ruby
cluster :gibbon do
  volumes(:hadoop_data) do
    size            300
    mount_point     '/hadoop_data'
    tags( :hadoop_data => true, :persistent => true, :bulk => true)
  end
  # the master uses a smaller volume and only stores data for the namenode
  facet :master do
    volumes(:hadoop_data) do
      size            50
      tags            tags.except(:hadoop_data).merge(:hadoop_namenode_data => true)
    end
  end
  # workers use the volume just as described
  facet :worker
    # ...
  end
end
```

note: This does not feel elegant at all.

class Facet
  attr_accessor  :cluster
  overlays       :cluster
  collection     :volumes, Volume, :members_overlay => :cluster
end

When a collection element is built on the

Deep Merge

Dynamic Resolution Rules

As you've seen, the core resolution rule is 'first encountered wins'. Simple and predictable, but sometimes insufficient.

Define your field with a :combine_with block (or anything that responds to #call) to specify a custom resolution rule:

Settings.option :flavors, Array, :of => String, :combine_with => ->(obj,attr,layer_vals){ ... }

Whenever there's a conflict, the block will be called with these arguments:

  • the host object,
  • the field name in question,
  • a list of the layer values in this order:
    • field default (if set)
    • lowest layer's value (if set)
    • ...
    • highest layer's value (if set)
    • object's own value (if set)

If you pass a symbol to :combine_with, the corresponding method will be invoked on each layer, starting with the lowest-priority:

Settings.option :flavors, Array, :of => String, :combine_with => :concat
# effectively performs
# layer(:charlie).flavors.
#   concat( layer(:bob).flavors ).
#   concat( layer(:alice).flavors )

Settings.option :flavors, Hash,  :of => String, :combine_with => :merge
# effectively performs
# layer(:charlie).flavors.
#   merge( layer(:bob).flavors ).
#   merge( layer(:alice).flavors )

Notes:

  • a field's default counts as a layer: the combiner is invoked even if just the default and a value are present.
  • normal (first-encountered-wins) resolution uses read_unset_attribute
  • dynamic (explicit-block) resolution uses read_attribute
  • there's not a lot of sugar here, because my guess is that concat'ing arrays and merging or deep-merging hashes covers all the repeatable cases.



Decisions

  • Layer does not catch Gorillib::UnknownFieldError exceptions -- a layered field must be defined on each part of the layer stack.
  • a field's default counts as a layer: the combiner is invoked even if just the default and a value are present

Provisional:

LayeredObject -- push some properties through to parent; earn predictable deep merge characteristics

  • holds a stack of objects

  • properties 'poke through' to get value with well-defined resultion rules

  • you can define a custom resolution rule if you define a property, or when you set its value

  • Properties are resolved in a first-one-wins

  • Defined properties can have resolution rules attached

  • You can put yourself in an explicit scope; higher properties are set, lower ones appear unset

Server overlays facet Facet overlays cluster Cluster overlays provider

don't prescribe any resolution semantics except the following:

  • layers are evaluated in order to create a composite, duping as you go.
  • while building a composite, when a later layer and the current layer collide:
    • if layer has merge logic, hand it off.
    • simple + any: clobber
    • array + array: layer value appended to composite value
    • hash + hash: recurse
    • otherwise: error

no complicated resolution rules allowed

This is the key to the whole thing.

  • You can very easily
  • You can adorn an object with merge logic if it's more complicated than that

(?mystery - duping: I'm not certain of the always-dup rule. We'll find out).

How do volumes overlay?

Which direction is overlay declared?

Configliere:

  • Default
  • File
  • Env var
  • Cmdline
  • Setting it directly