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

177 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.