diff --git a/02-DECISIONS/0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md b/02-DECISIONS/0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md new file mode 100644 index 00000000..9b79485d --- /dev/null +++ b/02-DECISIONS/0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md @@ -0,0 +1,90 @@ +--- +topic: what runs on it +status: proposed +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@.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.** + +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. +- **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` | +| 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.