How to define services and service classes

Synit services are started in response to run-service assertions. These, in turn, are eventually asserted by the service dependency tracker in response to require-service assertions, once any declared dependencies have been started.

So to implement a service, respond to run-service records mentioning the service's name.

There are a number of concepts involved in service definitions:

  • Service name. A unique identifier for a service instance.

  • Service implementation. Code that responds to run-service requests for a service instance to start running, implementing the service's ongoing behaviour.

  • Service class. A parameterized collection of services sharing a common parameterized implementation.

A service may be an instance of a service class (a parameterized family of services) or may be a simple service that is the only instance of its class. Service dependencies can be statically-declared or dynamically-computed.

A service's implementation may be external, running as a subprocess managed by syndicate-server; internal, backed by code that is part of the syndicate-server process itself; or user-defined, implemented via user-supplied code written in the configuration language or as other actor programs connected somehow to the system bus.

An external service may involve a long-running process (a "daemon"; what s6-rc calls a "longrun"), or may involve a short-lived activity that, at startup or shutdown, modifies aspects of overall system state outside the purview of the supervision tree (what s6-rc calls a "one-shot").

Service names

Every service is identified with its name. A service name can be any Preserves value. A simple symbol may suffice, but records and dictionaries are often useful in giving structure to service names.

Here are a few example service names:

  • <config-watcher "/foo/bar" $.>1
  • <daemon docker>
  • <milestone network>
  • <qmi-wwan "/dev/cdc-wdm0">
  • <udhcpc "eth0">

The first two invoke service behaviours that are built-in to syndicate-server; the last three are user-defined service names.

Defining a simple external service

As an example of a simple external service, take the ntpd daemon. The following assertions placed in the configuration file /etc/syndicate/services/ cause ntpd to be run as part of the Synit services layer.

First, we choose the service name: <daemon ntpd>. The name is a daemon record, marking it as a supervised external service. Having chosen a name, and chosen to use the external service supervision mechanism to run the service, we make our first assertion, which defines the program to be launched:

<daemon ntpd "ntpd -d -n -p">

Next, we mark the service as depending on the presence of another assertion, <default-route ipv4>. This assertion is managed by the networking core.

<depends-on <daemon ntpd> <default-route ipv4>>

These two assertions are, together, the total of the definition of the service.

However, without a final require-service assertion, the service will not be activated. By requiring the service, we connect the service definition into the system dependency tree, enabling actual loading and activation of the service.

<require-service <daemon ntpd>>

Defining a service class

The following stanza (actually part of the networking core) waits for run-service assertions matching a family of service names, <daemon <udhcpc ifname>>. When it sees one, it computes the specification for the corresponding command-line, on the fly, substituting the value of the ifname binding in the correct places (once in the service name and once in the command-line specification).

? <run-service <daemon <udhcpc ?ifname>>> [
    <udhcpc $ifname>
    ["udhcpc" "-i" $ifname "-fR" "-s" "/usr/lib/synit/udhcpc.script"]

This suffices to define the service. To instantiate it, we may either manually provide assertions mentioning the interfaces we care about,

<require-service <daemon <udhcpc "eth0">>>
<require-service <daemon <udhcpc "wlan0">>>

or, as actually implemented in the networking core (in lines 13–15 and 42–47), we may respond to assertions placed in the dataspace by a daemon, interface-monitor, whose role is to reflect AF_NETLINK events into assertions:

? <configure-interface ?ifname <dhcp>> [
  <require-service <daemon <udhcpc $ifname>>>

Here, when an assertion of the form <configure-interface ifname <dhcp>> appears in the dataspace, we react by asserting a require-service record that in turn eventually triggers assertion of a matching run-service, which then in turn results in invocation of the udhcpc command-line we specified above.

Defining non-daemon services; reacting to user settings

Only service names of the form <daemon name> are backed by external service supervisor code. Other service name schemes have other implementations. In particualr, user-defined service name schemes are possible and useful.

For example, in the configuration relating to setup of mobile data interfaces, service names of the form <qmi-wwan devicePath> are defined:

? <user-setting <mobile-data-enabled>> [
  ? <user-setting <mobile-data-apn ?apn>> [
    ? <run-service <qmi-wwan ?dev>> [
      <require-service <daemon <qmi-wwan-manager $dev $apn>>>
      $log ! <log "-" { line: "starting wwan manager", dev: $dev, apn: $apn }>

Reading this inside-out,

  • run-service for qmi-wwan service names is defined to require a <daemon <qmi-wwan-manager deviceName APN>> service, defined elsewhere; in addition, when a run-service assertion appears, a log message is produced.

  • the stanza reacting to run-service is only active when some <user-setting <mobile-data-apn APN>> assertion exists.

  • the stanza querying the mobile-data-apn user setting is itself only active when <user-setting <mobile-data-enabled>> has been asserted.

In sum, this means that even if a qmi-wwan service is requested and activated, nothing will happen until the user enables mobile data and selects an APN. If the user later disables mobile data, the qmi-wwan implementation will automatically be retracted, and the corresponding qmi-wwan-manager service terminated.


This first service name example is interesting because it includes an embedded capability reference using the $. syntax from the scripting language to denote the active scripting language environment dictionary.