|
|
|
@@ -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
|