synit-manual/src/operation/howto/define-services.md

177 lines
7.8 KiB
Markdown
Raw Normal View History

2022-05-14 13:12:12 +00:00
# How to define services and service classes
Synit [services](../builtin/index.md) are started in response to [`run-service`
assertions](../service.md#run-service). These, in turn, are eventually asserted by the service
dependency tracker in response to [`require-service`
assertions](../service.md#require-service), 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](../builtin/daemon.md), 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](../scripting.md) 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`][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`][s6-rc] calls a "one-shot").
[s6-rc]: https://skarnet.org/software/s6-rc/overview.html
## Service names
Every service is identified with its *name*. A service name can be any
[Preserves](../../glossary.md#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" $.>`[^interesting-service-name]
- `<daemon docker>`
- `<milestone network>`
- `<qmi-wwan "/dev/cdc-wdm0">`
- `<udhcpc "eth0">`
The first two invoke service behaviours that are [built-in to
`syndicate-server`](../builtin/index.md); 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/ntpd.pr` cause `ntpd` to be run as
part of the [Synit services layer](../synit-config.md#loading-of-core-and-services-layers).
First, we choose the service name: `<daemon ntpd>`. The name is a `daemon` record, marking it
as a supervised [external service](../builtin/daemon.md). 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:
```preserves
<daemon ntpd "ntpd -d -n -p pool.ntp.org">
```
Next, we mark the service as depending on the presence of another assertion, `<default-route
ipv4>`. This assertion is managed by the [networking core](../synit-config.md#networking-core).
```preserves
<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](../service.md#require-service), we connect the service definition into
the system dependency tree, enabling actual loading and activation of the service.
```preserves
<require-service <daemon ntpd>>
```
## Defining a service class
The following stanza (actually part of [the networking
core](../synit-config.md#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).
```preserves
? <run-service <daemon <udhcpc ?ifname>>> [
<daemon
<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,
```preserves
<require-service <daemon <udhcpc "eth0">>>
<require-service <daemon <udhcpc "wlan0">>>
```
or, as actually implemented in the networking core (in `network.pr` lines
[1315](https://git.syndicate-lang.org/synit/synit/src/commit/a43247fc96e906fbfdeb6e9a6e6d6e9a2f35f9e8/packaging/packages/synit-config/files/etc/syndicate/services/network.pr#L13-L15)
and
[4247](https://git.syndicate-lang.org/synit/synit/src/commit/a43247fc96e906fbfdeb6e9a6e6d6e9a2f35f9e8/packaging/packages/synit-config/files/etc/syndicate/services/network.pr#L42-L47)),
we may respond to assertions placed in the dataspace by [a daemon,
`interface-monitor`](https://git.syndicate-lang.org/synit/synit/src/commit/e9bf91e9de0d2724df15835fc6ea33cbdf23b564/packaging/packages/synit-config/files/usr/lib/synit/python/synit/daemon/interface_monitor.py),
whose role is to reflect [AF_NETLINK](https://en.wikipedia.org/wiki/Netlink) events into
assertions:
```preserves
? <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](../builtin/daemon.md) 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](../synit-config.md#wifi--mobile-data), service names of the form `<qmi-wwan
`*devicePath*`>` are defined:
```preserves
? <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](https://en.wikipedia.org/wiki/Access_Point_Name). If the user later disables mobile data,
the `qmi-wwan` implementation will automatically be retracted, and the corresponding
`qmi-wwan-manager` service terminated.
---
[^interesting-service-name]: This first service name example is interesting because it includes
an [embedded capability reference](../../glossary.md#reference) using the `$.` syntax from
the [scripting language](../scripting.md) to denote the active scripting language
environment dictionary.