From b421c73a5a26e2b4ad0744b6da25894a8cc7b580 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 15:38:16 +0200 Subject: [PATCH] ADR 0136: a step gates its module, not the machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0135 made a step something the mesh derives for any module that prepares its state, which turned ADR 0052's reach into a fault: a module whose database is briefly unreachable would stop every module declared after it on that machine — the fault issue 011 already removed for every other shape, and the reason the catalogue migrates itself at start rather than in a step. A step now stops the rest of its own module and nothing else; an action still gates the machine, because genesis is a row of them and they belong to no module. What was not attempted is reported as skipped rather than left to be inferred from silence. --- ...a-step-gates-its-module-not-the-machine.md | 106 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../01-to-be/32-what-a-module-declares.md | 5 +- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md diff --git a/02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md b/02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md new file mode 100644 index 0000000..b0e73eb --- /dev/null +++ b/02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md @@ -0,0 +1,106 @@ +--- +topic: what runs on it +status: accepted +date: 2026-09-28 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md +--- + +# 136. A step gates its module, not the machine + +## Context + +[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) made a run-once container a step the host +runs to completion, and gave it the same reach a failed action has: it stops everything the declaration +places after it. When the only steps on the mesh were a broker's seed and a forge's admin account, that +reach was invisible — the thing after the step was the container the step existed for, in the same +module. + +[ADR 0135](0135-a-module-version-prepares-its-state-before-it-runs.md) made a step something the mesh +derives for **any** module that prepares its state, and that turns the reach into a fault. A module +whose database is briefly unreachable now stops every module declared after it on that machine, for as +long as it is unreachable. + +**The host already rejected this for every other shape, and says why in its own loop.** From +[issue 011](../04-ISSUES/011-one-broken-module-blocks-every-other/00-report.md): + +> It used to stop at the first one, and that made one broken resource hold the whole machine hostage: a +> module declaring a package that does not exist meant every module ordered after it was never applied, +> for ever, and the mesh reported "failed" without saying that the rest had not been tried. A machine +> with one bad module and nine good ones ran none of the nine. + +Everything is attempted and every failure reported — except an action and a run-once step, kept as the +deliberate exceptions. So the mesh has two rules about the same question and the wider one is now +reachable by any module that declares a schema. + +**And it deadlocks a case the catalogue already named.** The catalogue migrates its own schema when it +starts rather than in a step, and says why in its code: *a schema step that had to reach the provider +over the overlay would block the very apply that brings the overlay up*. With a machine-wide gate that +is exactly right — the step fails, the apply stops, the overlay module after it is never applied, and +the next reconcile is blocked the same way. The module that most obviously wants a step could not have +one. + +## Decision + +**A step gates its own module.** A run-once container that does not complete stops the rest of *that +module's* resources and nothing else. Every other module on the machine is attempted, as every other +shape already is. + +**An action still gates the machine.** Genesis is a row of actions, each making the next possible, and +they belong to no module — there is nothing narrower for their reach to be. + +**What was not attempted is reported, not inferred from silence.** A skipped resource appears in the +machine's account of the apply as skipped, with the reason, because "not attempted" and "nothing to do" +are different answers and only one of them is somebody's to fix. + +**A module is the part of a resource's identity before the first dot**, which is how the mesh composes +them. What the mesh declares in its own right — a guard, an opening, the adoption's own resources — +belongs to no module, and its gate is therefore the machine's. + +## Options considered + +1. **Leave the reach as it is.** Rejected: it reintroduces, through a mechanism now derived for every + module, exactly the fault issue 011 removed. A mesh where one module's unreachable database stops a + machine converging is worse than one where that module alone is behind. +2. **Make preparation not a gate at all** — run it and carry on. Rejected: then a version serves against + a state nobody shaped, which is the whole of what ADR 0135 exists to prevent. +3. **Order every module's step before everything else on the machine**, so a gate stops nothing that + matters. Rejected: it inverts the order a module needs — its files and directories are declared before + its step because the step reads them — and it would still stop later modules. +4. **Let a module declare how far its step reaches.** Rejected: the answer is the same for every module, + and a field would let one be wrong about it. + +## Consequences + +**The catalogue can move to a step.** The reason it migrates at start — that a step blocks the apply +that would make its provider reachable — stops being true: the step fails, that module waits, the +overlay comes up, and the next reconcile prepares it. One shape for the whole mesh, which is what +ADR 0135 asked for and could not have had. + +**A module can sit behind while the machine is otherwise current.** That is the honest state and it is +what the report now says. It also means a preparation that never succeeds is a module that never +upgrades, quietly, until somebody reads the report — which is an argument for +[ADR 0134](0134-the-mesh-says-what-it-applied.md) rather than against this. + +**A module's resources must be ordered within the module for the gate to mean anything.** They already +are: the mesh composes a module's resources in the order its manifest declares them, and its own +workload comes after the files it reads. + +## How this is checked + +- **A failed step stops its module and nothing else.** A test with two modules: the one whose step + failed does not start its workload, the other starts, and the error still says the failure gated + something. It fails against the previous behaviour, which is how it was written. +- **An action still stops the machine.** The existing test for a failed action is unchanged, and a step + with no module in its identity — which is what genesis carries — takes the same path. +- **The report names what was skipped.** Asserted in the same test, because a gate nobody can see is + indistinguishable from a module that had nothing to do. + +## References + +- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the step this narrows +- [ADR 0135](0135-a-module-version-prepares-its-state-before-it-runs.md) — what made the reach reachable +- [issue 011](../04-ISSUES/011-one-broken-module-blocks-every-other/00-report.md) — the same fault, removed once already +- [ADR 0134](0134-the-mesh-says-what-it-applied.md) — how a module left behind becomes visible +- mesh-host `internal/apply` — the loop whose own comment argued this case for every other shape diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index b3e09e5..52f763b 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -216,6 +216,7 @@ python3 00-META/checks/index.py fail if stale - **0122** — [A seat is data the controller owns, and a rename is a database update](0122-a-seat-is-data-a-rename-is-a-database-update.md) - **0133** — [A module owns its migrations, and the mesh owns when they run](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) *(superseded)* - **0135** — [A module version prepares its state before it runs](0135-a-module-version-prepares-its-state-before-it-runs.md) +- **0136** — [A step gates its module, not the machine](0136-a-step-gates-its-module-not-the-machine.md) ### How it is built diff --git a/03-DESIGN/01-to-be/32-what-a-module-declares.md b/03-DESIGN/01-to-be/32-what-a-module-declares.md index 6e147e6..d2f556b 100644 --- a/03-DESIGN/01-to-be/32-what-a-module-declares.md +++ b/03-DESIGN/01-to-be/32-what-a-module-declares.md @@ -13,6 +13,7 @@ decisions: - 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md - 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md - 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md + - 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md - 02-DECISIONS/0134-the-mesh-says-what-it-applied.md --- @@ -264,7 +265,9 @@ queue of superseded ones, and a replayed older one is refused by sequence. its state to the shape that version needs — the same vocabulary as the entrypoints it declares for its tools and its provisioner, and nothing about how a machine runs it. The mesh runs that entrypoint as it runs the module's own code, to completion, in the module's own context, and a version whose preparation -did not succeed does not run: the rollout stops at the first machine that did not take it +did not succeed does not run: the step gates that module and nothing else on the machine +([ADR 0136](../../02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md)), and the rollout stops +at the first machine that did not take it ([ADR 0135](../../02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md), superseding [ADR 0133](../../02-DECISIONS/0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md)). Once per state, and the mesh derives what a state is: a consumer is a module on a machine, so what the -- 2.54.0