Merge pull request 'ADR 0136: a step gates its module, not the machine' (#162) from decision/0136-a-step-gates-its-module into main

This commit was merged in pull request #162.
This commit is contained in:
2026-09-28 13:38:18 +00:00
3 changed files with 111 additions and 1 deletions
@@ -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
+1
View File
@@ -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
@@ -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