CCS

Better configuration

(Arrow keys to navigate, ? for more help)

Config

  • Server app
  • UDP listener
  • port = 12345

Variability

  • Environment (dev/prod/labs/...)
  • Username
  • Deployed hostname
  • Product we're trading
  • More contrived things like "subproduct"

(Maybe you can already see this starting to suck...)

The Usual Approach

Naming conventions!


udp.prod.properties
udp.dev.properties
udp.defaults-mhellige.properties
                

[ini] sections!


[prod]
port=123
[dev]
port=456
                

Another property!

  • Another UDP listener
  • An embedded web server
  • More ports, more problems!

More Conventions!

In the filenames!


udp-a.prod.properties
udp-b.prod.properties
                

In the property names!


udp.a.port=123
udp.b.port=124
                

(But then where can I put defaults?)

A Better Way

It's all just "context"

env.prod env.dev

user.mhellige user.mgodbolt

host.'sud-chidev02'

udpListener.a udpListener.b webServer

Example


env.prod { // this is the production config
  udpListener.a : port = 123
  udpListener.b : port = 456
}
env.dev udpListener : port = 12345

webServer {
  port = 8080 // all environments
  user.mhellige : port = 8081 /* i prefer this one */
}
            
  • Conjunction by default
  • Nesting is just sugar
  • Most specific is highest priority.

Very Flexible

  • You decide how to group things and divide files
  • You decide which context matters and when
  • You decide how much to factor out vs repeat (@context)

What's the Idea?

We can construct a context path for each query and think of CCS as a pattern matching language for context paths.


desk.foo/exchange.NYSE/product.ABC > env.prod > dataCenter.unknown
 > appConfig.smq > user.produsername > host.rh-abcdef13
 > cpuType.haswell > module.core > threads > thread.TimerFiber
            

CCS lets us match on this path using conjunction, disjunction, and descendant operations, so with rules like


hungCommandThresholdMillis = 2000
cpuType.haswell : hungCommandThresholdMillis = 1000
thread.TimerFiber cpuType.haswell : hungCommandThresholdMillis = 5000
thread.TimerFiber cpuType.westmere : hungCommandThresholdMillis = 10000
            

We would find that

hungCommandThresholdMillis = 5000

Another view

  • We can envision a tree of contexts
  • Each branch extends the context with a new location or fact
  • All dimensions of variability are uniformly captured
  • Querying a property is equivalent to decorating this tree with key-value pairs
  • So what makes a good tree annotation language?

(Hence, an analogy with DOM trees and CSS)

Image here

Capturing context

  • CcsContext is everything
  • Loading gives a root context
  • Contexts can be constrained to give another context
  • Contexts can be queried for properties

Capturing context


CcsContext config = ... // from wherever
auto threadCfg = config.constrain("thread", {"TimerFiber"})
auto componentCfg = threadCfg.constrain("someComponent");
auto anotherCfg = threadCfg.constrain("anotherThing");
            

(It's pretty clear how this results in a tree...)

Some code


#include <ccs/ccs.h>
using namespace ccs;
using namespace std;

void initWeb(CcsContext config) {
  config = config.constrain("web");
  auto host = config.get<string>("host");
  auto port = config.get<int>("port");
  connect(host, port);
}
            

This isn't much different than declaring file- or class-specific loggers, and could be further automated when appropriate.

We use the query API by hand, but of course it could be driven by reflection or whatever, too...

We also capture context from external sources like initialization scripts, deployment information, etc...

This uniformity really pays off!

More Features

  • environment interpolation
  • conjunction, disjunction, descendant
  • nesting sugar
  • @context
  • @import
  • @constrain
  • @override

AND/OR


// ABC and DEF share an ID...
product.ABC, product.DEF : selfMatchPreventionId = 1234567
product.GHI : selfMatchPreventionId = 7654321

// in either of these environments, this host is special...
(env.dev, env.lab) host.'sul-chialg56' {
  useEfVi = true
}
            

Sugar


a b c.d : prop = true
a { c.d : prop = true }
a b {
  c.d {
    prop = true
  }
}
          

@context(a b)

c.d : prop = true
          

@import


@import 'module1.ccs'
@import 'module2.ccs'
 // imported only into given context...
module.timers : @import 'timers.ccs'
          

@constrain


// Pseudo-env for known overloaded machines to run them with less noise
env.lab (host.'dev-chi22', host.'dev-chi50',
    host.'dev-chi65', host.'dev-chi95') {
  @constrain env.overloaded
}

someTimeoutMs = 1000
env.overloaded : someTimeoutMs = 10000
          

@override


someTimeoutMs = 5000
thread.this : someTimeoutMs = 1000
thread.that : someTimeoutMs = 2000
cpuType.haswell {
  thread.this : someTimeoutMs = 500
  thread.that : someTimeoutMs = 800
}
// forget all of that in dev...
env.dev : @override someTimeoutMs = 100000
          

CCS Doesn't...

  • Type checking, beyond a bit of syntax
  • Schema validation or other static checks:
    1. Is everything required actually set?
    2. Is everything set actually used?
  • Sophisticated aggregate data values (list, maps, regex...)
  • Find usages in your code of a particular property
  • Reloading

Conclusions

Examples


host = "google.com"
port = 80
timeout = 1
env.lab { /* in lab, use this config */
  host = "lab-web"
  port = 8080
}
// bob likes to use this host
user.bob : host = "lab-proxy"
            

Examples


web {
  host = "google.com"
  port = 80
}
log {
  host = "localhost"
  env.lab : host = "lab-log-host"
  env.prod : host = "prod-log-host"
  port = 514
}
timeout = 1
// this host is really slow:
host.'dev-chi22' : timeout = 10