Merge pull request 'ADR 0269: what the mesh's own declaration stopped is started again when it goes' (#238) from decision/0269-what-the-meshs-own-declaration-stopped-is-started-again into main

This commit was merged in pull request #238.
This commit is contained in:
2026-10-09 00:20:32 +00:00
3 changed files with 127 additions and 1 deletions
@@ -80,6 +80,12 @@ records the state it first found the unit in, and undeclaring returns the unit t
manager: running before the mesh arrived, running after it leaves.
- **A unit the mesh started is stopped again**, and one it enabled is disabled again — the packet
filter a converge loaded, which returning to adopted unloads.
> **Narrowed, not replaced — 2026-10-09, by [ADR 0269](0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md).**
> "Never started on the way out" has one exception. The host now records what the mesh declared beside
> what it found. A unit found running or enabled that the mesh's own declaration held stopped or disabled
> is started or enabled again when that declaration goes, but only while it is still exactly as the mesh
> held it: a masked unit, or one changed since, is left as it is. The rest of this record stands.
- **Never started on the way out.** A unit the mesh stopped is not started again when its
declaration goes; starting something is a decision, and the operator makes it.
- **Unknown is left alone.** A record written before the host kept what it found says nothing
@@ -0,0 +1,110 @@
---
topic: what runs on it
status: accepted
date: 2026-10-09
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
---
# 269. What the mesh's own declaration stopped is started again when it goes
## Context
[ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md) gives a unit back as the
host found it, with one rule written against the direction of the rest: **never started on the way
out.** A unit the mesh stopped is not started again when its declaration goes, because "starting
something is a decision, and the operator makes it". When 0118 was written, the units the mesh stopped
were its own: a packet filter it had started, a guard it had loaded. Stopping a unit the machine ran
before the mesh came was not a case that existed.
It exists now. The `zfs` module owns the scrub schedule of the home server's pool. The machine already
had one: the distribution's `zfs-scrub-weekly@<pool>.timer`, enabled by hand, found running and enabled.
To leave exactly one schedule, the module declares that timer stopped and disabled, in the same
declaration that starts its own. Unassigning the module removes the module's own timer, which the mesh
made, and gives the distribution's back under 0118. That keeps it stopped and disabled. So the pool
would be left with **no scrub schedule at all**, and nothing would say so. The second review of
mesh-catalog #147 found this on 2026-10-09.
The host already knows half of what it needs. It records what it **found** (the unit's state and boot
before the mesh touched it) and keeps that from the first apply on. It did not record what the mesh
**declared**. So at removal it could not tell a unit that the mesh's own declaration stopped from one
that somebody stopped since.
## Considered Options
1. **Keep 0118 as it is, and have the module say a required step before unassigning** (re-enable the
distribution's timer by hand), with a health warning. Rejected: a step a person must remember is the
failure mode of the record ("failure must be loud", ADR 0227). The warning would come from a module
that is going away, so it would be said once and then never again.
2. **Start again, on removal, every unit found running.** Rejected: a unit somebody else stopped after
the mesh, an operator or another module, would be started behind their back. That is what 0118
refused, for good reason.
3. **Start again only what the mesh's own declaration stopped** — chosen. The host records what it
declared beside what it found. At removal, a unit found running that the declaration held stopped is
started again, and one found enabled that the declaration held disabled is enabled again.
## Decision
**The host records, for every service with a state, what the mesh declared: running or stopped, and
enabled or disabled where the declaration says. When the service's declaration goes, a unit that was
found running and declared stopped is started again, and one found enabled and declared disabled is
enabled again. The report says so as `restored`, and the plan says it before the apply does.**
**Only a unit exactly as the mesh held it.** At removal the unit must still be the stopped and disabled
the declaration held it to. A masked unit is somebody's word that it must not run, and is left as it is.
So is a unit somebody started or enabled since. In both cases the report says why. A restore that fails
(the start refused, or the unit not running after it) puts back what that attempt changed. It is reported
as *waiting*, never as an error that makes an apply fatal, and the next apply tries again.
0118's other rules stand:
- a unit found running and never stopped by the mesh is left running;
- a unit the mesh started is stopped again;
- a unit whose file the mesh wrote is the mesh's own;
- a unit with an unknown history is left alone. A record written before the host kept what it declared
says nothing about who stopped the unit, so nothing is started.
"Never started on the way out" now reads **never started on the way out, but for what the mesh's own
declaration stopped**. Starting something is still the operator's decision. Here the operator made it
before the mesh came, and the mesh's stopping it was the mesh's doing, which goes with the mesh.
## Consequences
- Unassigning the `zfs` module gives the home server back its distribution scrub timer, running and
enabled. A module may take over a machine's unit by holding it stopped, and its removal undoes that.
- A unit that is moved to another unit name (ADR 0118's "moved") is given back by the same rule.
- **A unit a person stopped by hand while the mesh held it cannot be told apart, and is started again.**
While the declaration holds a unit stopped and disabled, a person who also stops or disables it by hand
leaves no trace: the unit is in the state the mesh holds it to either way. At removal it is started and
enabled again, as it was found before the mesh. A person who wants it to stay off after the module goes
must disable it again afterwards, or mask it, which removal respects.
- **A restored timer catches up at once.** A timer with `Persistent=true` that was stopped past its
scheduled time fires as soon as it is started again. On the home server the distribution's weekly scrub
timer is such a timer, and a scrub of its pool took about 46 hours (2026-10-05 00:08 to 2026-10-06
22:06). Unassigning the `zfs` module after the timer's last run is more than a week back therefore
starts a scrub of about 46 hours, within the hour of the timer's random delay. Unassign it when that
load is acceptable.
- **Harder:** a unit the mesh stopped and that should *stay* stopped after the module goes (one that
was found running by accident) is now started again. Saying so needs a field on the resource, which
no module needs yet. Until one does, the plan's `restore … started … again` step is where it is seen.
## How it is checked
| Rule | Checked by |
|---|---|
| A unit found running and enabled, declared stopped and disabled, is started and enabled again when undeclared, and the report says `restored` | mesh-host `internal/apply/giveback_test.go`, `TestAUnitTheMeshStoppedAndDisabledIsGivenBackRunningAndEnabled` |
| A unit found stopped stays stopped; one never stopped by the mesh is left as it is | `TestOnlyWhatTheMeshsDeclarationStoppedIsStartedAgain` |
| A record without what was declared starts nothing | `TestARecordWithoutWhatWasDeclaredStartsNothing` |
| A masked unit, or one started or enabled since, is left as it is, and the report says why | `TestAMaskedOrChangedUnitIsLeftAloneOnRemoval` |
| A failed restore waits and is never fatal, and the record stays for the next apply | `TestAFailedRestoreWaitsAndIsNeverFatal` |
| Every call in `internal/system` that names a service resource's unit passes `--` before it | mesh-host `internal/system/dashdash_test.go` |
| The plan says it before the apply does | `TestThePlanSaysAStoppedUnitIsStartedAgain` |
| 0118's existing rules | mesh-host `internal/apply/found_test.go`, unchanged and passing |
## References
- [ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md), which this widens.
- mesh-host: the host's record (`store.Applied.Declared`), `startUnitBack` in `internal/apply/apply.go`,
and the plan's step in `internal/apply/plan.go`.
- mesh-catalog #147 (the `zfs` module's scrub schedule) and its second review, 2026-10-09.
+11 -1
View File
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-host]
updated: 2026-10-02
updated: 2026-10-09
decisions:
- 02-DECISIONS/0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md
- 02-DECISIONS/0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md
- 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
@@ -186,6 +187,15 @@ the firewall it was found with is in force and who retired it. It retires that f
converged apply, not once, records *found inactive* apart from *disabled by the mesh*, and says when the
step was skipped. *How it is checked:* ADR 0168's table.
**What the mesh's own declaration stopped goes back with it** — revision, 2026-10-09
([ADR 0269](../../02-DECISIONS/0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md)). The
node-engine records, for a service with a state, what the mesh declared beside what it found. When the
declaration goes, a unit found running or enabled that the declaration held stopped or disabled is
started or enabled again, but only while it is still exactly as the mesh held it. A masked unit, or one
changed since, is left as it is, and the report says why. A restore that fails waits for the next apply
and is never fatal. Every call that names a service's unit passes `--` before it. *How it is checked:*
ADR 0269's table.
**Found reaches every kind that can touch what the machine has**
([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record
keeps its mode and owner, a unit present with no record keeps its state and boot setting, a