ADR 0135 supersedes 0133: a module version prepares its state before it runs
Two faults in 0133, both caught on review. It put the declaration on a container — one resource kind the host applies — so every author would restate the machine's arrangement and a module's own lifecycle would be tied to how its artifact happens to run. A module declares entrypoints for its tools and its provisioner; preparing its state is the same vocabulary and nothing about a runtime. And it derived the scope from the machine, which the facts already answer: a consumer is a module on a machine (issue 022, migration 0015), so what the mesh provisions is per consumer. A module on three machines has three databases, there is no shared state to race over, and the lock obligation 0133 invented was for a situation the mesh does not produce. The level question HAL answered with stages dissolves — the scope of preparation is the scope of the state, and the mesh knows it. 0133 keeps its reasoning and gains a pointer; design 32 and issue 133 name the live record.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
status: superseded
|
||||
date: 2026-09-28
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
|
||||
superseded-by: 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
|
||||
---
|
||||
|
||||
# 133. A module owns its migrations, and the mesh owns when they run
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-28
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes: 0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md
|
||||
---
|
||||
|
||||
# 135. A module version prepares its state before it runs
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) settled who runs a
|
||||
module's migrations and when, and it said so in the wrong vocabulary. It put the declaration on a
|
||||
*container* — "a container may declare steps to run before it" — and derived the scope of the work from
|
||||
the *machine*. Both are wrong at the level a module author works at, and the second is wrong on the
|
||||
facts.
|
||||
|
||||
**A container is one resource kind the host applies.** A module has code, state and a version; whether
|
||||
its artifact is an image, a bundle or something later is the mesh's business. The module-facing
|
||||
vocabulary for a module's own code already exists and has nothing to do with a container runtime: a
|
||||
module declares **entrypoints** — this file is my tools, this file is my provisioner — and the mesh runs
|
||||
them. A manifest that says "run this container with these arguments, and here are the volumes and
|
||||
environment again" has an author writing down the machine's business twice.
|
||||
|
||||
**And the scope is not the machine's to decide, because the mesh already decided what a state is.** A
|
||||
consumer is a module *on a machine* (migration 0015, from
|
||||
[issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md)):
|
||||
the mesh derives a login per consumer and the provider creates a database owned by exactly that login
|
||||
([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)). So a module on three machines is three
|
||||
consumers, three credentials and three databases. There is no shared state for two machines to race over,
|
||||
and ADR 0133's central caveat — that a module's migrations must take a lock because two machines might
|
||||
migrate at once — describes a situation the mesh does not currently produce.
|
||||
|
||||
That correction makes the whole "level" question HAL answered with stages disappear: the scope of
|
||||
preparation is the scope of the state, and the mesh knows it.
|
||||
|
||||
What the earlier record got right and this one keeps: the module owns the work, the mesh owns the moment,
|
||||
the gate is the guarantee, migrations stay forward-only, and none of it can be inferred from inside an
|
||||
artifact. What produced it also stands — the control plane was replaced with a build carrying a migration,
|
||||
nothing applied it, and for three quarters of an hour every build was refused by the store with one line
|
||||
that reached only whoever was waiting on a reply
|
||||
([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
|
||||
|
||||
## Decision
|
||||
|
||||
**A module version declares an entrypoint that prepares its state.** One name in the manifest, in the
|
||||
same vocabulary as the entrypoints it already declares for its tools and its provisioner. No container,
|
||||
no command line, no environment, no mounts — those are how a machine runs the module's code, and the
|
||||
module already said that once.
|
||||
|
||||
**The mesh runs it as it runs that module's own code, to completion, in the module's own context.** Every
|
||||
binding, credential and setting the module's code would receive, because it *is* the module's code. How a
|
||||
machine does that is the host's business and stays there: for an image artifact it is the step
|
||||
[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) already defines, and a later kind of artifact
|
||||
changes the host, not the manifest.
|
||||
|
||||
**Preparation gates the version.** A version whose preparation did not succeed does not run — anywhere.
|
||||
Since the rollout already sends machines one at a time and stops at the first that does not take a
|
||||
version, a preparation that fails stops the rollout there, leaving every other machine on the version
|
||||
that works.
|
||||
|
||||
**Preparation is scoped to the state, and the mesh derives that scope.** State the mesh provisions is per
|
||||
consumer — a module on a machine — so preparation happens once per consumer. State the module keeps on
|
||||
the machine is per machine, which is the same answer. A module that holds an exclusive seat has one of
|
||||
itself, so its preparation happens once by definition. No level, no election, no cross-node ordering, and
|
||||
no lock obligation invented for a race the mesh does not create.
|
||||
|
||||
**Once per version per state.** A version bump attempts preparation once against each state it has; the
|
||||
module's own runner decides there is nothing to do, which is what a runner with a version table does
|
||||
anyway. A retry after a partial failure runs it again, so the work is the module's to make safe against
|
||||
that — the one obligation no design can remove.
|
||||
|
||||
**Forward-only and additive.** Preparation runs while the previous version is still serving, so a
|
||||
migration that removes or renames what the old code reads breaks the mesh in the window between the two.
|
||||
|
||||
**Declared, never inferred.** The control plane cannot see inside an artifact, so a module that ships
|
||||
migrations and declares no entrypoint is not refusable at registration. It breaks on its first upgrade,
|
||||
and this record says so rather than implying a check that cannot exist.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **A container declares steps before it** — [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md).
|
||||
Superseded, not because the mechanism is wrong but because the *declaration* is in the wrong place: it
|
||||
makes every module author restate the machine's arrangement, and it ties a module's own lifecycle to
|
||||
one resource kind. The host-side mechanism it named is retained and is now an implementation detail.
|
||||
2. **Each module prepares itself when it starts** — what the catalogue does today. Rejected: a schema
|
||||
failure becomes a crash loop rather than a stop, nothing in the declaration says the module has a
|
||||
state to prepare, and the version serves the moment it starts rather than after the state is right.
|
||||
3. **The mesh applies migrations itself**, with a driver and a version table per store type. Rejected:
|
||||
the mesh would have to know one store from another, hold another module's credentials and reach a
|
||||
machine with them, which [ADR 0005](0005-the-node-host.md) forbids. It is also what forces a stage
|
||||
system: something central has to decide where the work happens.
|
||||
4. **A hook lifecycle** — pre-build, post-build, pre-deploy, post-deploy. Rejected: a declaration is a
|
||||
desired state reconciled forever, so there is no deploy moment to hook. "Pre-deploy" is exactly this
|
||||
record; pre- and post-build are what a recipe and the artifact list already are; "post-deploy" names
|
||||
nothing that happens.
|
||||
5. **A declared level** — once per module, or once per assignment. Rejected: the mesh already knows what a
|
||||
state is, so asking an author to choose is asking them to restate a fact the mesh holds, with a chance
|
||||
of contradicting it.
|
||||
6. **Record a preparation level per module in the store.** Rejected for the reason ADR 0133 gave and this
|
||||
record keeps: gating makes the invariant true by construction, and a level is a second account of the
|
||||
same fact.
|
||||
|
||||
## Consequences
|
||||
|
||||
**An author's whole contract is one line, once.** Write the migration in the module's code, name the
|
||||
entrypoint that runs it, and every later version rolls out as: build, prepare, run — with nothing
|
||||
per-version to remember and nothing about the machine to restate. That is the property this exists for.
|
||||
|
||||
**Three hand-written steps in the catalogue collapse**, and the control plane's own migrate step stops
|
||||
repeating its server's environment and mounts.
|
||||
|
||||
**The catalogue's self-preparation becomes the exception to remove.** One shape, and the mesh's own
|
||||
control plane is not an exception either.
|
||||
|
||||
**A module scaled across machines with one shared state is not expressible**, and this record does not
|
||||
make it so. The mesh gives each consumer its own state; a deliberately shared one is a different
|
||||
provision model, and the place the "once, mesh-wide" question would genuinely return. Named here so it is
|
||||
a decision when it happens rather than a surprise.
|
||||
|
||||
**There is still no readiness-gated step.** Only an action carries `verify`; nothing declares that a
|
||||
service answers, so preparation that must happen *after* something is serving — seeding through its own
|
||||
API — remains unexpressible.
|
||||
|
||||
**Genesis keeps its own action.** At birth there is no control plane to derive anything, which is what
|
||||
[ADR 0067](0067-genesis-is-a-pivot.md) says about that moment.
|
||||
|
||||
## How this is checked
|
||||
|
||||
- **The composition carries the preparation, in the module's own context.** A test on a node's composed
|
||||
declaration: a version declaring a preparation entrypoint is preceded by it, and what it is given
|
||||
equals what the module's own code is given — asserted equal rather than written twice, which is the
|
||||
drift the superseded shape invited.
|
||||
- **A preparation that fails stops the version.** The host does not go past a step that did not complete,
|
||||
and the rollout stops at the first machine that did not take a version. Both are existing behaviours
|
||||
with existing tests; the test for preparation asserts the two together — the machine does not run it,
|
||||
and the machines after it are left alone.
|
||||
- **Once per version per state.** A test that a second convergence of the same version prepares nothing,
|
||||
and that a new version prepares again.
|
||||
- **The mesh's own control plane declares one.** The case that failed on 2026-09-28 is the case the tests
|
||||
cover, rather than a case a comment says is covered.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — what this supersedes, and why
|
||||
- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the host-side step that implements it for an image artifact
|
||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), [issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md) — a consumer is a module on a machine, which is what makes the scope derivable
|
||||
- [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — a digest is the record that something happened
|
||||
- [ADR 0005](0005-the-node-host.md) — the control plane decides and never touches a machine
|
||||
- [ADR 0134](0134-the-mesh-says-what-it-applied.md) — what makes a failed preparation visible
|
||||
- [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md) — the failure that produced both records
|
||||
@@ -214,7 +214,8 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0120** — [A roster fact carries its format as a template: the mesh owns the data, the module owns the format](0120-a-roster-fact-carries-its-format-as-a-template.md)
|
||||
- **0121** — [A system seat is named for its scope, and a module may define its own](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||
- **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)
|
||||
- **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)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
Reference in New Issue
Block a user