The event system¶
The event system lets a service react to something that happens elsewhere on the
system by running a 66 command on itself — restart when a file changes, reload when a
signal reaches another service, start on a schedule, or start when a dependency dies. The
reaction is declared in the frontend service file and compiled into the service resolve
file at parse time. The daemon that runs the rules at runtime is
66-eventd.
This page documents the model and its runtime behaviour. The per-field syntax —
which key goes in which section, its allowed values — is part of the frontend reference in
66-frontend: the Type = event source keys live in
[Main], the reactor keys in the
[Event] section.
A worked example¶
Say your configuration management tool rewrites /etc/ssh/sshd_config, and you want sshd
to re-read its configuration every time that happens. Two pieces are involved: a source
that notices the file change, and a reactor that acts on it.
First, the source — a service whose only job is to watch the file:
# frontend: sshd-config-watch
[Main]
Type = event
Description = "watch /etc/ssh/sshd_config"
EventType = inotify
Watch = /etc/ssh/sshd_config
On = ( IN_CLOSE_WRITE )
Then, sshd itself gains an [Event] section that subscribes to that source:
# frontend: sshd
[Main]
Type = classic
Description = "OpenSSH daemon"
[Start]
Execute = ( /usr/bin/sshd -D )
[Event]
EventType = inotify
From = ( sshd-config-watch )
Do = reload
At runtime: 66 start sshd-config-watch arms the watch; when
/etc/ssh/sshd_config changes, 66-eventd sees that sshd has
From = ( sshd-config-watch ) and runs 66 reload sshd. That is the whole model — a source
emits, one or more reactors act.
Sources and reactors¶
Every event flows from a source to one or more reactors.
- A reactor is an ordinary service (
classic,oneshotormodule) that gains an[Event]section. When its source fires, it runs a66command on itself and/or raises a named event. - A source is a service of the new
eventtype. It runs no process; it only emits events. Its configuration lives directly in[Main], there is no[Event]section. There are three families of source:- inotify — watch a filesystem path;
- schedule — fire on a cron/calendar expression;
- timer — fire on a relative interval.
Two more reactor families need no configured source at all:
- service — react to the up/down/crash transitions of any supervised service;
- signal — react to a signal routed by
66to a supervised service; - user — react to a name raised by 66 emit, or by another reactor's
Emitkey.
A reactor acts on itself — and on nothing else¶
This is the load-bearing rule of the whole model. A reactor's Do has no target: there
is no key naming the service to act upon, because the service acted upon is always the one
declaring the rule. When the event fires, 66-eventd runs 66 <do> <the reactor> — the
reactor's own name, never another's.
Nothing else in 66 can reach into a service this way, and it is the opposite of how reactive control is usually built. The common shape is a separate trigger file that names the service it must activate, or a hook or dispatcher script that restarts it from the outside; either way the rule that acts on a service lives somewhere that service never mentions. In 66 the direction is reversed: the service states what happens to it.
Three consequences follow, and they are the reason the model is worth using:
- No action at a distance. The frontend of a service lists everything event-driven that can happen to it. You never have to grep the rest of the system to find out what restarts it at 3 a.m.
- Subscribing never edits someone else's file. Adding a reactor touches exactly one frontend — its own. The source is not modified, and neither is any other subscriber. A package can ship a service that reacts to a system event without patching a single file it does not own.
- Watching is not acting. Naming a source in
Fromobserves it; it never starts, stops or otherwise touches that source — seeFromis a watch, not a dependency.
The one outward-facing key is Emit, and it is deliberately indirect: it raises a
name, not a command, and it acts on nobody. Whoever wants to react to that name subscribes
to it, in their own frontend, with their own Do. So even the cascades below are made of
services that each decided for themselves — no service is ever acted upon by a rule it does
not carry.
One source, many reactors¶
A source is fan-out: any number of reactors may name the same source in their From (or,
for user, key on the same name), and each reacts in its own way — its own Do, its own
Emit, its own On filter. One timer can drive a backup and a metrics flush and a log
rotation; one IN_MOVED_TO on a certificate directory can reload every daemon that serves it;
one 66 emit cert-renewed reaches every subscriber at once. The source knows nothing about its
reactors — you add a subscriber, never touch the source.
This is where the model departs from unit-per-trigger designs. Where a trigger file names the single service it activates, driving several services from one trigger means interposing a group that pulls them all in, and giving each of them a different action is not expressible at all — the trigger and the acted-upon service are 1:1. In 66 the coupling is 1:N by construction, and each reactor keeps its own verb.
About the event type and arming¶
An event service is not supervised: it has no [Start] section and no running
process. On such a service, 66 start and 66 stop are not
process supervision — they are arm and disarm messages sent to 66-eventd. 66 start
tells the daemon to begin watching (open the inotify watch, program the timer, …); 66 stop
tells it to stop. Everything else (enable, dependencies, trees) works as usual.
One rule at most, and it is opt-in¶
The event system is entirely optional. The overwhelming majority of services declare no event rule at all. When a service does, it carries at most one rule: a service is either one source, or one reactor — never both, and never several rules at once.
Declaring a source ([Main] Type = event)¶
A source is a whole frontend of type event. The EventType key selects the family; the
remaining keys depend on it. Field syntax is documented in
66-frontend.
EventType = inotify (source)¶
| Role | Section | Required keys | Optional keys |
|---|---|---|---|
| source | [Main] |
EventType, Watch, On |
— |
# frontend: sshd-config-watch
[Main]
Type = event
Description = "watch /etc/ssh/sshd_config"
EventType = inotify
Watch = /etc/ssh/sshd_config
On = ( IN_CLOSE_WRITE IN_MOVE_SELF )
EventType = schedule (source)¶
| Role | Section | Required keys | Optional keys |
|---|---|---|---|
| source | [Main] |
EventType, Expression |
Timezone |
# frontend: nightly-3am
[Main]
Type = event
Description = "fires every night at 03:00 Paris time"
EventType = schedule
Expression = "0 0 3 * * ?"
Timezone = Europe/Paris
EventType = timer (source)¶
| Role | Section | Required keys | Optional keys |
|---|---|---|---|
| source | [Main] |
EventType, Every |
— |
# frontend: heartbeat
[Main]
Type = event
Description = "fires every 30 seconds"
EventType = timer
Every = 30s
Declaring a reactor (the [Event] section)¶
A reactor is an ordinary supervised service that adds an [Event] section. The EventType
selects which family of source it subscribes to and, for service/signal/user, which
On vocabulary applies. When the trigger fires the reactor performs a Do, raises an
Emit, or both — at least one is required. Field syntax is documented in
66-frontend.
EventType = service (reactor)¶
| Role | Section | Required keys | Optional keys |
|---|---|---|---|
| reactor | [Event] |
EventType, From, On/OnAll, Do/Emit |
Propagate |
# frontend: backend
[Main]
Type = classic
Description = "backend worker"
[Start]
Execute = ( /usr/bin/backend )
[Event]
EventType = service
From = ( rabbitmq )
On = ( down )
Do = stop
Emit = backend-down
OnAll fires only when several conditions hold at once — useful to wait on a whole set of
dependencies:
[Event]
EventType = service
From = ( auth cache db )
OnAll = ( auth:up cache:down db:up )
Do = restart
EventType = signal (reactor)¶
| Role | Section | Required keys | Optional keys |
|---|---|---|---|
| reactor | [Event] |
EventType, From, On/OnAll, Do/Emit |
Propagate |
# frontend: sshd
[Main]
Type = classic
Description = "OpenSSH daemon"
[Start]
Execute = ( /usr/sbin/sshd -D )
[Event]
EventType = signal
From = ( network )
On = ( SIGHUP SIGUSR1 )
Do = reload
EventType = user (reactor)¶
| Role | Section | Required keys | Optional keys |
|---|---|---|---|
| reactor | [Event] |
EventType, On, Do/Emit |
Propagate |
A user reactor has no From — it listens for a name, wherever that name comes from.
A name is raised in three ways: by hand with 66 emit <name>, by another reactor's Emit
key, or by 66 itself at a system milestone (see Lifecycle events raised by
66 below). The second form is how reactions chain. In the pair below, backend reacts to
rabbitmq going down by stopping itself and raising backend-down; alerter listens
for that name:
# frontend: backend
[Main]
Type = classic
Description = "backend worker"
[Start]
Execute = ( /usr/bin/backend )
[Event]
EventType = service
From = ( rabbitmq )
On = ( down )
Do = stop
Emit = backend-down
# frontend: alerter
[Main]
Type = oneshot
Description = "page the on-call engineer"
[Start]
Execute = ( /usr/local/bin/page-oncall )
[Event]
EventType = user
On = ( backend-down )
Do = start
The same alerter also fires if an operator runs 66 emit backend-down from a script — the
user family is the general-purpose entry point into the event system.
Lifecycle events raised by 66¶
66 raises a few user events on its own, at the moments in the system's life that a service
most often needs to hang off. You subscribe to them exactly like any other user name — an
[Event] section with EventType = user and On = ( <name> ). You never emit them
yourself with 66 emit; 66 does, from inside 66 boot, the shutdown daemon and
66 env.
These names all carry a dot. The dotted form is reserved for events 66 raises itself; the
names you raise with 66 emit or Emit are bare (cert-renewed, backend-down). The two
namespaces never collide, and a dot in an On line tells you at a glance the event comes from
the system, not from another service.
| Event | Raised when | Typical use |
|---|---|---|
boot.done |
boot has finished — every enabled tree is started | bring up something that must wait for a fully-booted system |
boot.failed |
boot could not start every enabled tree | raise an alert, open an emergency shell |
shutdown.begin |
a shutdown or reboot has been scheduled, before any service is stopped | flush state, notify a peer, quiesce a daemon cleanly |
env.<variable> |
66 env published variable — env.DISPLAY, env.XAUTHORITY |
start a service the moment the value it needs becomes available |
unenv.<variable> |
66 env withdrew variable |
tear a service down when the value it depends on goes away |
boot.done fires after the last enabled tree is up, which is exactly the hook a getty
wants — a login prompt should appear only once the machine has finished booting. Instead of
wiring the getty into the boot dependency graph, you subscribe it to the event:
# frontend: tty1
[Main]
Type = classic
Description = "getty on tty1"
[Start]
Execute = ( execl-cmdline -s { agetty 38400 tty1 } )
[Event]
EventType = user
On = ( boot.done )
Do = start
At boot the getty is armed but not started — a Do = start reactor is armed
idle: it waits at rest and runs its Execute only when its event
arrives. When boot.done fires, 66 starts it. Because a source is fan-out, every getty and
every other "wait for boot" service subscribes to the same boot.done without boot having to
know they exist.
shutdown.begin is the mirror image, and its timing matters: it fires while services are
still up, during the shutdown grace period, so a reactor still has a live system to act on.
That window is short — keep the reaction quick (a flush, a signal), not a long job that would
be cut off when the teardown proceeds.
EventType = inotify / schedule / timer (reactor)¶
| Role | Section | Required keys | Optional keys |
|---|---|---|---|
| reactor | [Event] |
EventType, From, Do/Emit |
Propagate |
A reactor to a configured source carries no On: the condition lives in the source and
is authoritative. To react differently, create a distinct source.
EventType is still stated explicitly here, even though From already names the source. It
is redundant for these three families but kept on purpose: every reactor is then uniform and
self-describing — you read the family from the section itself, without having to open the
source's frontend.
That redundancy is checked, not assumed: 66 parse refuses a reactor whose EventType
disagrees with the family of the source named in From, and refuses a From that is not a
Type = event source at all. This is not cosmetic — EventType is the runtime dispatch key,
so a mismatch would arm cleanly and then never fire, silently.
# frontend: backup — runs every night, driven by the nightly-3am source above
[Main]
Type = oneshot
Description = "nightly backup"
[Start]
Execute = ( /usr/local/bin/backup )
[Event]
EventType = schedule
From = ( nightly-3am )
Do = start
A reaction is the bare 66 command, dependency chain included. A rule that must stay narrow
opts out with Propagate:
# postgresql frontend: re-reads its access rules alone when pg_hba.conf changes
[Main]
Type = classic
Description = "database server"
[Start]
Execute = ( /usr/bin/postgres -D /var/lib/postgres/data )
[Event]
EventType = inotify
From = ( pg-hba-watch )
Do = reload
Propagate = false
postgresql is required by every service that talks to the database. Without that last line,
editing one access control line would SIGHUP the whole application stack; here only
postgresql re-reads its rules.
A cascading example: certificate rotation¶
Renewing a TLS certificate is a classic orchestration headache: several daemons must pick up
the new file, some can reload it in place, some must be fully restarted, and a front
proxy must come last so it never briefly serves the old certificate. With Do and Emit
this is a short chain.
The renewal tool (e.g. certbot) announces the event from its deploy hook:
66 emit cert-renewed
Each daemon reacts in the way that suits it — note that nginx reloads while postfix
restarts, decided per reactor:
# frontend: nginx
[Event]
EventType = user
On = ( cert-renewed )
Do = reload
Emit = nginx-reloaded
# frontend: postfix
[Event]
EventType = user
On = ( cert-renewed )
Do = restart
The front proxy must reload after nginx, so it does not chase cert-renewed directly —
it waits for the name nginx raises once its own reload is done:
# frontend: haproxy
[Event]
EventType = user
On = ( nginx-reloaded )
Do = reload
The result is a fan-out (nginx + postfix both react to cert-renewed) with a one-step
ordering constraint (haproxy after nginx), and each service keeps its own reload
-versus-restart decision — the very point where a single propagated “reload” command falls
short. Ordering across the chain is expressed by who emits what, so keep the chain shallow
and acyclic.
Case studies: reactive control other init systems make hard¶
Every mainstream init and supervision system expresses a static dependency graph — start order, "A needs B" — well. The part with no clean native form elsewhere is the reactive one: at runtime, react to something that happens to one service by acting on another. The cases below are each reproduced and verified on 66; each closes with an exact note on how other systems fare, so the comparison is concrete rather than rhetorical.
React to a daemon that gives up for good¶
A daemon exhausts its crash budget — MaxDeath deaths within a
MaxDeathInterval window (milliseconds) — and the
supervisor marks it failed. You want to record it, or start a standby, once:
# frontend: flaky
[Main]
Type = classic
Description = "a daemon that keeps crashing"
[Start]
Execute = ( flaky-daemon )
MaxDeath = 3
MaxDeathInterval = 30000
# frontend: alerter
[Main]
Type = oneshot
Description = "record the failure, once"
[Start]
Execute = ( /usr/local/bin/alert "flaky is down for good" )
[Event]
EventType = service
From = ( flaky )
On = ( failed )
Do = start
66 start alerter arms the reactor and brings flaky up (its From source). flaky dies, is
retried, dies again; on the third death in the window the supervisor gives up and marks it
failed. alerter fires once and flaky stays failed — the reactor watches
flaky without depending on it, so its Do = start acts on alerter alone and never revives
flaky (see From is a watch, not a dependency).
Compared with others. A failure hook that only fires on a terminal failed state is blind
to the common case: with automatic restart enabled, a crash that is retried never reaches that
state, so a service that crashes and is restarted all day long triggers nothing. Hooks of that
shape also tend to de-duplicate on the failure edge, running once and staying silent while the
service keeps flapping. s6 and runit have no failure-to-action mechanism at all — the default is
to restart forever, and any cross-service reaction is a hand-written wrapper. 66 exposes both
the terminal failed and every intermediate down transition as reactable events, and a
reactor re-fires each time.
Restart a client to reconnect when its backend restarts¶
When a shared backend — dbus, a database, a message broker — restarts, clients hold dead connections and must be bounced to reconnect:
# frontend: client
[Main]
Type = classic
Description = "reconnects when the backend restarts"
[Start]
Execute = ( the-client )
[Event]
EventType = service
From = ( backend )
On = ( up )
Do = restart
Whenever backend reaches up — an operator restart, or the backend recovering on its own —
client restarts to reconnect. The two are decoupled in the dependency graph (66 restart
backend does not touch client, and client's restart does not drag backend back up); the
coupling is purely the event rule.
Compared with others. The usual answer is pre-declared propagation rather than reaction:
a dependent is restarted when an operator restarts the named service, or is stopped when the
service it is bound to stops — but nothing brings it back when that service returns, which is
precisely the moment a client needs to reconnect. That is why restarting a shared bus is
routinely described as leaving its clients broken until they are bounced by hand. runit has no
mechanism — you script it (sv hup <dependent> in the dependency's finish); s6-rc
re-evaluates dependencies only at database-update time. None offers a lightweight "when the
backend is available again, restart me."
Reload on an atomic config or certificate swap¶
Config and certificate tools update a file atomically: they write a temporary file in the
same directory and rename() it into place, so a reader never sees a half-written file. You
want to reload when the real file is replaced:
# frontend: cfg-watch — the source
[Main]
Type = event
Description = "watch a config directory for an atomic replace"
EventType = inotify
Watch = /etc/myapp
On = ( IN_MOVED_TO )
# frontend: myapp — the reactor
[Main]
Type = classic
Description = "reload on config change"
[Start]
Execute = ( myapp )
[Event]
EventType = inotify
From = ( cfg-watch )
Do = reload
A rename() into /etc/myapp raises IN_MOVED_TO, which fires the reload; because the mask is
explicit, an unrelated in-place write does not. (rename() is atomic only within one
filesystem — which is exactly why the temporary file must live in the same directory as its
target.)
Compared with others. A file watcher that binds to the file's own inotify descriptor
instead of its directory's IN_MOVED_TO does not fire on an atomic rename-into-place — it
misses the very pattern every safe config update uses, since the new file is a different inode.
66 lets you name the mask and the directory yourself, so the swap is exactly what you watch.
Any inotify-based watcher, 66 included, inherits the kernel's limits — a change made on a
remote NFS mount is not seen. s6, runit and OpenRC have no built-in file watching at all;
you bolt an inotifywait loop onto a manual SIGHUP.
One event, per-service policy: certificate rotation¶
The certificate-rotation cascade above is the
fourth case: one 66 emit cert-renewed drives a fan-out where nginx reloads and postfix
restarts — each reactor picking its own verb — with a one-step ordering constraint carried
by Emit.
Compared with others. Propagation mechanisms carry a single verb — typically reload — so a group where some daemons reload and others must restart cannot be driven from one trigger. In practice the whole per-service matrix ends up encoded in the renewal tool's deploy hook, far away from the services it acts on and owned by whoever maintains that hook. 66 keeps each reactor's policy on the consumer, where it belongs.
From is a watch, not a dependency¶
The first two cases rely on a single property, worth stating on its own. For a
service/signal reactor, From is a subscription, not a runtime dependency:
- At arm time it behaves like an ordering edge — 66 supervises the source before it arms the reactor, so the daemon exists for the reactor to read its current state (this is what lets a condition that already holds fire at once; see firing immediately on arm).
- At runtime it carries no propagation: stopping or restarting the source does not tear
the reactor down, and a reaction's
Do = start/restartstarts the reactor without re-pulling the source. That is why the direct rules above act on their source without the reactor ever reviving or dragging it.
A tick reactor (inotify/schedule/timer) is different: there the source is a passive
Type = event watch that exists only to serve its reactors, so From is a dependency — the
watch is armed with the reactor. A user reactor has no From at all; it keys on an
Emitted or 66 emitted name, which is the way to react to a source you must
not couple to.
One more timing note, independent of all this: a reactor's Execute runs when the reactor is
armed (66 start), not when its condition fires — except for a Do = start/restart
reactor, which is armed idle and runs its Execute only when the event
arrives (see Emit timing). Put the work that must happen at the event in a
Do = start/restart reactor, not in the Execute of a plain source-watcher.
Synthesis: keys per EventType¶
| EventType | Source ([Main]) |
Reactor ([Event]) |
|---|---|---|
inotify |
Watch + On |
From + Do/Emit |
schedule |
Expression (+ Timezone) |
From + Do/Emit |
timer |
Every |
From + Do/Emit |
service |
(not a source) | From + On/OnAll + Do/Emit |
signal |
(not a source) | From + On/OnAll + Do/Emit |
user |
(not a source) | On + Do/Emit (no From) |
Every reactor carries EventType and at least one of Do / Emit. Every source carries
[Main] Type = event. Each key's full definition — mandatory-ness, syntax and valid values
— is in 66-frontend.
Runtime behaviour¶
Declaring a rule is only half the story; the other half is when and whether it actually
fires. 66-eventd applies several rules that are easy to miss on paper. This section is the
authoritative description of them.
Arming and disarming¶
For a Type = event source, 66 start and 66 stop do not
supervise a process — they arm and disarm the source at the daemon (open the inotify
watch, program the timer or schedule).
For a reactor the two verbs are not symmetric, and that asymmetry is the single most common point of confusion:
- 66 start arms the reactor — from then on it reacts to its events.
- 66 stop brings the service down but leaves the reactor armed. The rule
stays registered at
66-eventdand keeps reacting, so aDo = startreactor you stopped comes back up on its next event.stopacts on the running service, never on the reaction. - 66 free — which stops the service and unsupervises it — is what
disarms the reactor: it leaves the scandir and
66-eventd, and reacts no more. For amodule, a singlefreedisarms every member reactor in the same pass.
In short, to make a reactor stop reacting you must free it — a plain stop is not
enough. This is also why Recovery after 66-eventd restarts (below) re-arms a
merely-stopped reactor: it is still in the scandir, so the daemon picks it up again on the
next repopulate.
A reactor whose action is Do = start or Do = restart is a further special case: on
66 start it is armed, not launched. Both verbs bring the service up when the event
fires, so the parser forces such a service down and starting it only registers the rule —
the service comes up later, when its event fires. Every other verb is the opposite: stop,
reload, reconfigure and free act on a running service, so 66 start launches the
reactor normally and it reacts from up. In 66 status an armed-and-idle
reactor shows the WAITING state.
When a reactor actually fires — state gating¶
When the trigger matches, 66-eventd still checks the reactor's current state before
running its Do. The command is only issued when it would do something:
Do |
Fires only when the service is… |
|---|---|
start |
anything except up or done |
stop, reload |
anything except down or failed |
restart |
(any state — always fires) |
reconfigure, free |
(any state — always fires) |
The gate is built from two sets: a service is active when it is up or done, and
inert when it is down or failed; the five transitional states (starting,
stopping, finishing, restarting, waiting) are neither. start fires only on a
non-active service, stop/reload only on a non-inert one, and restart fires
unconditionally — it is the verb to use when the action must run on every matching event
regardless of where the service currently sits.
The most common surprise follows from the start row: Do = start does not run on a
service that is already up or done — it is a silent no-op (logged as inhibited),
because start means “bring it up” and an already-active service is there. This does
not keep a oneshot/module reactor from reacting repeatedly: such a reactor
re-arms to waiting after each firing — it never parks in done — so a Do = start
or Do = restart reactor fires again on every matching event, running its Execute exactly
once per event. If the reactor's own status cannot be read at that moment, every Do except
reconfigure/free is inhibited.
Parking in waiting does not lose the firing that just happened: the date of the last run
is kept beside the state, so 66 status tells a reactor that already did
its work from one still waiting for its first event:
$ 66 status numlockx
Status : enabled, waiting since 1h 13min by event (last run 1h 13min ago)
$ 66 status
├─numlockx (pid=waiting(done), state=Enabled, type=oneshot, tree=global)
A reactor that has never fired since it was armed reads (never run), and shows a bare
waiting in the list. 66 stop clears that memory, as does a reboot.
Firing immediately on arm¶
Because 66 supervises each From source before it arms the reactor — as an establishment
edge for a service/signal reactor, or as a dependency for a tick reactor (see From is a
watch, not a dependency) — 66-eventd reads the source's
current state at arm time: a service reactor whose awaited condition already holds fires
at once, instead of waiting for the source's next transition. You do not have to arrange for the
event to happen strictly after the reactor is armed.
On versus OnAll¶
On is an OR: the reactor fires as soon as one listed condition matches. OnAll is an
AND evaluated on a point-in-time snapshot of every source's committed status — it
fires only when all conditions hold at the same instant. A subtlety worth remembering: if
one source's state cannot be determined at that instant, the OnAll fails (an
unprovable condition is treated as unmet), not merely “not yet true”.
reload is not a signal¶
This is the least intuitive rule. 66 reload and a Do = reload action deliver the
service's reload-signal to its process, but they do not announce a signal to the event
system. Consequently a reload never wakes a signal reactor listening on, say,
On = ( SIGHUP ) — even though the reload may itself send SIGHUP under the hood.
A signal reactor fires only for a signal routed by 66 (66 signal, or
a Do that maps to a signal) to a service that was up when the signal was routed. A
signal aimed at an already-down service announces nothing (it killed nothing).
The in-flight latch¶
While a reactor's action is running — forked but not yet reaped — the reactor absorbs every
re-trigger until that action completes. This covers the short window between issuing the
command and the target's status being written back, so a burst of events cannot stack up a
pile of duplicate actions. The latch applies to Emit-only reactors too.
The anti-loop backstop¶
If a reactor fires 10 times within 10 seconds, 66-eventd decides it is runaway and
squelches it: the reactor is disarmed and destroyed, with a warning in the log. It is
not throttled and it does not re-arm itself — a fresh 66 start is
required to bring it back. The backstop also applies to Emit, which is what ultimately
breaks a cyclic Emit chain.
Emit timing¶
A reactor that carries both Do and Emit raises its event after the Do has
committed (once the action process is reaped), not the instant the trigger matches. An
Emit-only reactor raises its event as soon as the filters pass.
Tick sources at runtime¶
timer, inotify and schedule sources are started and stopped like any Type = event
service, but they are not supervised by the scandir and are not reference-counted by
the reactors that name them — each is armed by its own start. 66-eventd writes their
status itself: done when armed, down when disarmed, failed when the underlying watcher
dies.
A source can die at runtime: if an inotify source's watched path is removed (the kernel
drops the watch), or a timer/schedule watcher errors, that one source goes failed while
every other source keeps ticking. Re-create the path and 66 start it again to re-arm — the
death is not permanent.
Freeing a source¶
For a tick source (inotify/schedule/timer), which its reactors depend on, 66 free
<source> (without -P) propagates to the reactors that require it and disarms them in
cascade — you free the source, and the rules that fed on it go away with it.
A service/signal reactor only watches its From service (see From is a watch, not
a dependency), so freeing that service does not disarm
the reactor: the rule stays armed on a now-absent source — it simply never fires again — until
you 66 free the reactor itself. Disarming is always done on the reactor.
Recovery after 66-eventd restarts¶
66-eventd is itself supervised and may restart (a crash, a scandir reconfigure). On
startup it repopulates from the scandir: it re-arms every supervised event source and
every reactor it finds, best-effort. Arming is idempotent, so nothing is double-armed. You
do not need to re-start your rules by hand after the daemon bounces.
Notes and pitfalls¶
- One rule per service. A frontend holds at most one source or one reactor. To react
to several unrelated events, split the logic across services, or chain them with
Emit. signaledvssignal.On = ( signaled )on aservicereactor fires when the target dies from a signal; asignalreactor fires when a signal is routed to the target by66. They are opposite directions.reloaddoes not wake asignalreactor. Seereloadis not a signal above — the single most common source of confusion.Do = starton an already-active (upordone) service is a no-op. But an armedoneshot/modulereactor re-arms towaitingbetween events, so this does not stop it firing again — it runs itsExecuteonce per event, and66 statusstill reports when it last ran. See state gating.- Quartz
?. Ascheduleexpression must carry?on day-of-month or day-of-week; it has no@reboot. Emitchains can loop.Emit = atriggering a rule whoseEmit = btriggering a rule whoseEmit = ais a cycle.66-eventdbreaks such loops with the backstop; still, keep chains short and acyclic.
Prototype¶
Source (in a [Main] Type = event service):
[Main]
Type = event
Description = ""
EventType =
Watch =
On = ()
Expression = ""
Timezone =
Every =
Reactor (an [Event] section added to a normal service):
[Event]
EventType =
From = ()
On = ()
OnAll = ()
Do =
Emit =
Propagate =