diff --git a/02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md b/02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md index c5958f13..7e11f52b 100644 --- a/02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md +++ b/02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md @@ -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 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..a2b4ae7a --- /dev/null +++ b/02-DECISIONS/0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md @@ -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@.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. diff --git a/03-DESIGN/01-to-be/05-the-node-host.md b/03-DESIGN/01-to-be/05-the-node-host.md index fc95481e..567ccd96 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -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