Skip to content

The configuration file

sen run my_config.yaml starts a kernel from a YAML file. That file is how you say which components exist, which objects they publish, how fast they run and how the kernel itself behaves. For most users it is the part of Sen they touch most often, and the only part they touch without writing C++.

This page is the reference for it. Command line tools documents the sen commands themselves.

Sections

A configuration file has these top-level keys. All of them are optional, and a file with none of them starts a kernel that does nothing.

Key What it holds
include Other YAML files to merge into this one
load Components that already exist as libraries, to be loaded
build Components for the kernel to assemble out of your packages
kernel How the kernel itself runs

Start with the distinction between load and build. load brings in a component someone wrote in C++: the term, influx, jsonrpc, or one of yours. build asks the kernel to construct a component for you out of packages and a list of objects, with no component code of your own. Most projects use both: load for the term, build for their own objects.

both sections together
load:
  - name: term
    group: 2
    open: [ local.counters ]

build:
  - name: counterComponent
    group: 3
    freqHz: 2
    imports: [ my_counter ]
    objects:
      - name: myCounter
        class: my_counter.CounterImpl
        bus: local.counters
        step: 5

load: components that already exist

Each entry needs a name, which is the component's registered name. A name repeated in load is a hard error, not a last-one-wins.

Beyond name, an entry may carry any key from ComponentConfig and any number of keys the component itself defines. open above is one of the latter: the term reads it, the kernel does not know what it means.

build: components the kernel assembles

Each entry needs a name, and again a repeated name is a hard error. The keys the kernel treats specially are:

Key Meaning
freqHz How often the component runs its cycle
imports Packages to load, so the kernel can find your classes
objects The objects to instantiate

freqHz

The cycle rate, converted internally to a period. Neither of these is obvious from the file:

  • Omitting it is not an error. The kernel logs component frequency not set. Defaulting to 30 Hz and continues at 30 Hz. If you have never seen that warning it is because your configurations set the key. A missing freqHz therefore fails quietly in the log rather than at startup.
  • freqHz has to be positive. Zero or a negative number is refused at startup with invalid frequency value. There is no floor above zero and no ceiling, so freqHz: 0.5 is valid and gives a two-second cycle.

imports

A list of package names. The kernel needs this to resolve the class values below; without the import the class simply is not found. Note that importing a package is not the same as the loader finding its library. That is LD_LIBRARY_PATH, or PATH on Windows, and is covered in Create your first package.

objects

A list of objects to instantiate. Three keys are structural and the rest are initial property values:

Key Required Meaning
name yes Instance name, unique within its component and bus
class yes Qualified implementation class, <package>.<Class>
bus no Where to publish it; omit it and the object is not published on a bus

A repeated object name within a component is a hard error, as it is for components themselves. And bus must be exactly two dot-separated parts, a session and a bus. So my.tutorial is valid, my.tutorial.extra and a bare tutorial both fail at startup with invalid bus address.

Every other key sets a property's initial value. Static properties must be given one here, because they cannot be assigned later.

objects:
  - name: firstGrade
    class: school.ClassroomImpl
    bus: school.primary
    studentsBus: school.primary   # a property of ClassroomImpl
    defaultSize: 5                # likewise

What the component actually receives

The kernel forwards configuration keys on to the component as its own parameters, and the exclusion list is short: everything except group, objects and imports is forwarded.

That is wider than it looks. name and freqHz are forwarded as well as consumed, and so are the ComponentConfig keys. So a component may read freqHz itself if it wants to, and a parameter of your own named priority will collide with the thread priority. Pick another name.

ComponentConfig

These keys may appear on any load or build entry. They configure the thread the component runs on and the queues around it.

struct ComponentConfig
{
  priority    : Priority,     // thread priority
  stackSize   : u32,          // thread stack size in bytes, 0 means default
  group       : u32,          // the group where to run the component
  cpuAffinity : u64,          // CPU affinity mask
  inQueue     : QueueConfig,  // queuing of inbound information
  outQueue    : QueueConfig,  // queuing of outbound information
  sleepPolicy : SleepPolicy   // configurable component sleep policy
}
Key Notes
priority lowest, nominalMin, nominalMax or highest
stackSize Thread stack in bytes; 0 means the platform default
group Startup ordering, see below
cpuAffinity A CPU mask, so 0x5 is cores 0 and 2
inQueue, outQueue See Queues
sleepPolicy See Sleep policy

group is the one that changes behavior most. Components start in ascending group order, and a group is fully up before the next begins. That is why the term is conventionally in group 2 and your own components in group 3: the term is ready and has its buses open before objects start appearing on them. Groups are not priorities, and they do not affect scheduling while the system runs. They do decide shutdown order: the kernel stops and unloads groups from the highest number down, so a component in group 2 outlives one in group 3 and the term is the last thing to go.

Queues

// Component queue eviction policy
enum QueueEvictionPolicy: u32
{
  dropOldest,  // when full, drop the least recent element
  dropNewest,  // when full, drop the most recent element
}

struct QueueConfig
{
  evictionPolicy : QueueEvictionPolicy,  // what to do when the queue is full
  maxSize        : u64                   // 0 means unbounded
}

Queues are unbounded by default (maxSize: 0). Setting a bound means deciding what to discard when it is reached, which is what evictionPolicy is for: dropOldest keeps the freshest data and dropNewest keeps the earliest.

build:
  - name: myComponent
    group: 3
    freqHz: 100
    inQueue:
      maxSize: 1000
      evictionPolicy: dropOldest

Sleep policy

// Use the native system sleep
struct SystemSleep;

// Be more precise at the expense of some CPU cycles.
struct PrecisionSleep
{
  veryCoarseGrainSleepTime : Duration,  // First set of sleeps. If 0 defaults to 7ms
  coarseGrainSleepTime     : Duration   // Second set of sleeps. If 0 defaults to 1ms
}

// Component sleep policy
variant SleepPolicy
{
  PrecisionSleep,
  SystemSleep
}

A variant, so you pick one shape or the other. SystemSleep hands waiting to the operating system. PrecisionSleep spins down through two coarser sleeps before the final wait, which costs CPU and buys timing accuracy; a zero in either field means its default, 7 ms and 1 ms respectively. Components built by the kernel default to PrecisionSleep.

The kernel section

// This struct is reachable over the wire through Kernel.getConfig() and is embedded in SenData,
// and the encoding is positional, so adding a field here shifts what follows it either way.
struct KernelParams
{
  runMode             : RunMode,                // how to run the kernel
  appName             : string,                 // optional name of the application
  bus                 : string,                 // where to publish the kernel objects (defaults to local.kernel)
  clockBus            : string,                 // where to publish the virtual clock (if any), defaults to bus
  clockName           : string,                 // the name of the virtual clock (if any), defaults to 'clock'
  clockMaster         : bool,                   // if time is virtualized, publish a master clock to clockBus
  logConfig           : sen.kernel.log.Config,  // logging configuration
  crashReportDir      : string,                 // where to store crash reports (defaults to the temp dir)
  crashReportDisabled : bool,                   // if true, no reports are generated
  lockMemoryPages     : bool,                   // keep process pages memory-resident
  sleepPolicy         : SleepPolicy,            // configurable sleep policy of the kernel component
  compatibilityMode   : CompatibilityMode,      // what to do with a remote type that does not match
  limitCpuIdleLatency : bool                    // keep the CPUs out of the deep idle states they are slow to leave
}

Everything here has a default, so the section can be omitted entirely.

Key Notes
runMode See below
appName Names the application; optional
bus Where kernel objects are published. Defaults to local.kernel
clockBus Where a virtual clock is published. Defaults to whatever bus is
clockName Defaults to clock
clockMaster Under virtual time, publish a master clock to clockBus
logConfig Pattern, level, sinks, loggers and whether a backtrace is printed on failure
crashReportDir Where crash reports and minidumps go. Defaults to the system temporary directory. See Crash reports
crashReportDisabled sen run arms nothing, so neither a report nor a minidump is written. See Crash reports
lockMemoryPages Keeps the process resident, so it cannot be paged out
sleepPolicy As above, for the kernel's own component

Run modes

enum RunMode: u8
{
  realTime,            // components execute using system time
  virtualTime,         // components execute using discrete steps (set by the user)
  virtualTimeRunning,  // same as virtualTime, but continuously advancing the time
  startAndStop,        // starts and stops (useful for smoke tests)
}

realTime is the default shape: components run against the system clock. Under virtualTime the kernel advances in discrete steps you drive yourself, which is what makes a run reproducible within a process; virtualTimeRunning advances those steps continuously instead of waiting to be told. startAndStop brings everything up and takes it straight back down, which is a cheap smoke test that a configuration is valid.

Environment variables

Any value may pull from the environment with @env(NAME). A missing variable is a runtime error, so use the two-argument form @env(NAME,DEFAULT) when you want a fallback.

Spaces around the comma are fine. The default itself has to be letters, digits or underscores, so the dot in @env(BUS,local.kernel) stops it matching. Text that does not match is left alone. You get no error and no fallback, just the literal @env(BUS,local.kernel) as your value. The pattern also does not work across two lines.

load:
  - name: term
    group: 2
    open: @env(MY_BUS)                       # throws if MY_BUS is unset
  - name: @env(MY_COMP,defaultComponent)     # falls back if MY_COMP is unset
    group: 3

The pattern escapes with backslashes written immediately before the @, and what happens depends on whether there is an odd or an even number of them:

  • Even: rendered as half as many backslashes, and the variable is substituted.
  • Odd: rendered as half of what remains after removing one, and the substitution does not happen; the @env(...) text is left as it is.
with MY_COMP=FooComponent
someEscape1: \@env(MY_COMP,defaultComponent)      # -> @env(MY_COMP,defaultComponent)
someEscape2: \\@env(MY_COMP,defaultComponent)     # -> \FooComponent
someEscape3: \\\@env(MY_COMP,defaultComponent)    # -> \@env(MY_COMP,defaultComponent)
someEscape4: \\\\@env(MY_COMP,defaultComponent)   # -> \\FooComponent

Combining files

include merges other files into this one. Inclusion is recursive, and paths are relative to the top-level configuration file rather than to the file doing the including.

my_setup.yaml
include:
  - shell.yaml
  - ether.yaml

build:
  - name: myComponent
    group: 3
    freqHz: 10
    imports: [ my_package ]
    objects:
      - name: myObject
        class: my_package.MyClassImpl
        bus: my.tutorial
        prop1: someValue

Lists are concatenated, so two files each contributing a load entry give you both. Where the same setting is defined twice, the file doing the including wins. That is what makes overriding compact. Name only what you want to change:

the same setup, with one property overridden
include:
  - my_setup.yaml

build:
  - name: myComponent
    objects:
      - name: myObject
        prop1: someOtherValue

Nothing else needs repeating. group, freqHz, imports, class and bus all survive from the included file.

Getting your editor to check the file

Sen can generate a JSON schema from your data model, and an editor pointed at that schema will flag unknown keys and wrong types as you type, and complete your own class and property names. This is the part of configuring Sen where the tooling already does the work, so it is worth wiring up.

If you built your package with add_sen_package, the schema already exists. Generation is on by default and lands in ${PROJECT_SOURCE_DIR}/schemas, which SCHEMA_PATH controls and NO_SCHEMA turns off (see CMake). Otherwise, generate one from your STL:

# a schema for a package
sen generate json package stl stl/my_counter/counter.stl \
    -i stl -c my_counter.CounterImpl -o my_counter.json

Do not skip -c. It lists the implementation classes you wrote, which is what lets the schema complete and check the class: values in your objects list. Without it the schema's class list comes out empty. Components use sen generate json component stl with -n for the component name instead.

Then combine every schema your configuration might mention into one file:

sen generate json schema my_counter.json other_package.json -o schema.json

From CMake, sen_combine_schemas(OUTPUT <file> SCHEMAS <files...>) does the same thing as a build step, which is how this repository assembles the schema its own examples use.

Finally, point the configuration at it. Every example configuration that ships with Sen carries this as its first line:

# $schema: ../base/schema.json

load:
  - name: term
    group: 2

How that line is picked up depends on your editor. JetBrains IDEs read the # $schema: comment directly, which is why the examples are written that way. The YAML extension for VS Code looks for its own form, # yaml-language-server: $schema=./schema.json, or a yaml.schemas mapping in your settings. Both consume the same generated file.

Regenerate it when the model changes

The schema is generated from your STL, so it goes stale exactly like any other generated artifact. Under CMake that is handled for you. By hand, it is a step to remember. A schema describing last week's model will confidently flag a property you added yesterday.

See also