Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3523617d3 | ||
|
|
0231974226 | ||
|
|
92c029d10e | ||
|
|
2a60da821d | ||
|
|
8ca09c70d6 | ||
|
|
db71b83711 | ||
|
|
9143d0b7c1 | ||
|
|
24a51a8e53 | ||
|
|
f0d7f91d90 | ||
|
|
8578a06ca8 | ||
|
|
86083f9c9d | ||
|
|
63d328147e | ||
|
|
fead0ea440 | ||
|
|
df503d1cff | ||
|
|
c2fc829822 | ||
|
|
8d8e5c9a7e | ||
|
|
affba60b79 | ||
|
|
9ddbc4c68e | ||
|
|
a972db91f0 | ||
|
|
029698fdc8 | ||
|
|
895c2afad1 | ||
|
|
d362155401 |
@@ -0,0 +1,141 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
|
||||
---
|
||||
|
||||
# 189. The store keeps what the records name, and a maintenance step holds its writers still
|
||||
|
||||
## Context
|
||||
|
||||
The mesh's artifact store has never collected anything
|
||||
([issue 108](../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md)).
|
||||
Every build pushes another layer set; nothing has ever removed one. The predecessor ran a routine
|
||||
on a timer — stop the registry, collect, start it — and the conversion carried the settings that
|
||||
routine depends on without the routine, because the routine was a script beside the module and not
|
||||
a resource in it. The store now holds fifty-three repositories on the machine that serves
|
||||
everything else, and the only outcome of leaving it is a full disk reported as somebody else's
|
||||
failure.
|
||||
|
||||
Three things stood in the way, and the issue names all three.
|
||||
|
||||
**Nothing in the mesh's vocabulary expresses a maintenance window.** The collector requires every
|
||||
writer stopped while it runs. A `run-once` step runs *beside* containers, not instead of them, and
|
||||
a scheduled step is the same container on a cadence. There is no way for a module to say *hold this
|
||||
container of mine still while this runs*.
|
||||
|
||||
**Deletion is not enabled, and the door it would be enabled on has no accounts.** The store is
|
||||
internal, reached by name over the overlay, trusted because being on that network is the permission
|
||||
([ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)). The predecessor
|
||||
kept deletion behind an authenticated door, which it could, having one.
|
||||
|
||||
**Nothing says what may be removed.** The registry's own answer — collect everything no tag names —
|
||||
is wrong here. The mesh pushes each artifact under one moving tag and pins machines by digest, so
|
||||
every build but the newest is untagged and some machine may still be running it.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. Deletion is enabled on the store's one door, and the overlay stays the permission.** The
|
||||
objection dissolves on inspection: that door **already accepts a push**, and a writer who can push
|
||||
can replace any tag in the store with anything it likes. Delete takes nothing a push did not
|
||||
already have, and the machines that can reach the door are the ones the mesh's own filter admits
|
||||
([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)). Putting an authenticated
|
||||
door in front of deletion while leaving push open would be a lock on the window beside an open
|
||||
door, and it would cost the thing ADR 0082 bought: a store every machine can reach without a
|
||||
credential to distribute first.
|
||||
|
||||
**2. The mesh deletes what it made and no longer keeps; the store reclaims the bytes.** Two halves,
|
||||
each doing what only it can.
|
||||
|
||||
The **mesh** decides. It does not need to enumerate the store to do it — it has never put anything
|
||||
there it did not record, so **every digest it could remove is already in its own build records**.
|
||||
It deletes those manifests through the store's door, by digest, and remembers that it did.
|
||||
|
||||
The **store** reclaims. A deleted manifest frees no bytes until the registry's own collector walks
|
||||
the storage with nothing writing to it, so the module declares that collector as a scheduled step
|
||||
with the server held still for its duration. Plain collection, not `--delete-untagged`: what the
|
||||
mesh keeps is still a manifest in the store, so it is still referenced, so its blobs stay — the
|
||||
dangerous flag is not needed at all once the mesh is the one deciding.
|
||||
|
||||
**3. What the mesh keeps, stated as three reasons rather than a number.** A digest is kept because:
|
||||
|
||||
- **a definition names it** — every artifact reference in any module's current recorded manifest,
|
||||
which is what the mesh would hand a machine now. No age limit: this is the floor;
|
||||
- **the mesh can still go back to it** — every artifact of the **five most recent successful
|
||||
builds** of each module, so a release that turns out wrong has somewhere to return to;
|
||||
- **nothing else.** An artifact older than that, which no definition names, is what the store is
|
||||
carrying for no stated reason.
|
||||
|
||||
A digest the mesh did not record making is never touched. That is not a safety margin, it is the
|
||||
whole rule restated: the mesh removes what it put there and can account for, and the images genesis
|
||||
pushed before any record existed are exactly what this must not reach
|
||||
([04-ISSUES/102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md), F4).
|
||||
|
||||
**4. A scheduled step may hold its module's own containers still while it runs** —
|
||||
`while-stopped`, naming resource ids in the same module. The host stops each, runs the step, and
|
||||
starts them again **whatever the step did**, including when it failed or the host was interrupted.
|
||||
Three boundaries:
|
||||
|
||||
- **Its own module's containers only.** A module that could quiesce a neighbour could stop the
|
||||
mesh; a maintenance window is a statement about one service's own insides.
|
||||
- **Scheduled steps only, not `run-once`.** At apply time the host already has a window: the
|
||||
declaration is applied in order and a step gates what follows, so a one-time offline migration
|
||||
says *before* rather than *instead of*. A recurring window is the case order cannot express.
|
||||
- **Restoring is not conditional.** A step that fails must leave the service running; the whole
|
||||
risk of this field is a window that never closes.
|
||||
|
||||
**5. The sweep runs where the records change — after a build the mesh recorded.** That is the
|
||||
moment new bytes landed and the moment the keep set moved, and it needs no new timer. The
|
||||
store's collection runs nightly, because reclaiming is slow and the thing it reclaims is already
|
||||
unreferenced.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Disk stops growing without bound on the machine that serves the mesh. That is the whole point
|
||||
and it has no other way to be true.
|
||||
- A machine behind by more than five builds of a module, which recreates a container, cannot pull
|
||||
what it was running. It is already a machine the mesh reports as behind, and the answer is the
|
||||
one the mesh already gives it: the current declaration. Stated here rather than discovered.
|
||||
- The store is a little less of a museum. A digest in an old build record may no longer be
|
||||
fetchable, and the record still says what that build made — the record is history, not an
|
||||
index of what is on disk. The collected mark is kept beside it so the two can be told apart.
|
||||
- `while-stopped` is a second thing the host does to a container it did not start this pass. It is
|
||||
deliberately the narrowest form: the module's own, by id, restored unconditionally.
|
||||
- The store is briefly unavailable each night, for as long as collection takes. Everything that
|
||||
pulls from it retries; nothing in the mesh treats a momentary store as a failure
|
||||
([ADR 0185](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)).
|
||||
- **An apply arriving during the window reopens it**, because the host's rule for a container it
|
||||
finds stopped is to replace it, and `while-stopped` is the first thing that makes a stopped
|
||||
container intentional. Found by reading this before it merged, recorded as
|
||||
[issue 224](../04-ISSUES/224-an-apply-reopens-a-maintenance-window-by-recreating-what-it-held-still/00-report.md)
|
||||
rather than fixed here: the two candidate fixes — the window takes the apply lock, or the apply
|
||||
learns which containers are held — are each a decision with its own cost, and neither belongs
|
||||
inside this record. Nothing is worse than it was; the store has never collected at all.
|
||||
- **The sweep is bounded**: at most two hundred artifacts and sixty seconds per build, stopping at
|
||||
the first refusal, because it runs inside somebody's build. What is left over is offered again
|
||||
next time. The store stops growing from the first sweep; it does not empty in one.
|
||||
|
||||
## How this is checked
|
||||
|
||||
- The host: a scheduled step with `while-stopped` stops the named containers before the run and
|
||||
starts them after; it starts them again **when the step fails**; it refuses an id that is not a
|
||||
container of the same module, its own id, and `while-stopped` on a `run-once` step. Each refusal
|
||||
is tested for what it says, not only that it says something.
|
||||
- The controller: given build records and current manifests, the keep set holds every reference a
|
||||
manifest names and every reference of the five most recent builds per module, and nothing else;
|
||||
a reference the mesh never recorded is never in the delete set; a delete that answers 404 is
|
||||
recorded as collected rather than retried forever.
|
||||
- The sweep is tested against a fake store that records what it was asked to delete, so what is
|
||||
asserted is the decision and not the registry's behaviour.
|
||||
- Live: the store's size before and after the first nightly collection, read from the machine.
|
||||
|
||||
## References
|
||||
|
||||
- [issue 108 — the registry has no garbage collection](../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md)
|
||||
- [ADR 0082 — the registry is reached by name and trusted by the overlay](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
|
||||
- [ADR 0053 — a step that runs on a schedule](0053-a-step-that-runs-on-a-schedule.md)
|
||||
- [ADR 0156 — an artifact is what a build produces, and the store is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||
- [design 32 — what a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md)
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0067-genesis-is-a-pivot.md
|
||||
---
|
||||
|
||||
# 200. Genesis pivots to the controller as a container, and the first push hands it to a process
|
||||
|
||||
## Context
|
||||
|
||||
The controller is Go, compiled to one static binary, and is the last of the mesh's own programs a
|
||||
machine runs from an image ([issue 213](../04-ISSUES/213-the-controller-is-a-go-program-run-in-a-container/00-report.md)).
|
||||
[ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||
§1 says a module's own code is bundles, never an image, and §3 that a service bundle is a `process` the
|
||||
host runs. The handover exists: a process may name the container it `replaces`, and the host removes
|
||||
that container only after the process has stayed up across two checks; two controllers are safe
|
||||
together for that moment, the second standing by on the controller's consumers and every plan held by
|
||||
one lock.
|
||||
|
||||
What stands in the way is genesis ([ADR 0067](0067-genesis-is-a-pivot.md)), which
|
||||
[issue 223](../04-ISSUES/223-a-new-mesh-installs-its-controller-as-a-container/00-report.md) found
|
||||
assumes an image and a container at every step from its third: it builds the controller's image,
|
||||
starts a temporary controller from it, publishes it, finds the controller's container in the pivot
|
||||
declaration, and from then on talks to the controller through it. A process's bundle is fetched from
|
||||
the artifact store, and genesis raises the artifact store only after the pivot.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Raise the artifact store before the pivot**, publish the controller's bundle to it, and talk to
|
||||
the controller from the host's side. Rejected for now: it reorders genesis around a store that is
|
||||
itself a module the controller deploys, and rewrites the steps that talk to the controller — a
|
||||
larger change to the one path that is exercised least, to remove a container that exists for
|
||||
minutes.
|
||||
2. **Pivot to the controller as a container, as today, and let the first push hand it over to the
|
||||
process**, through the handover that already exists. Chosen.
|
||||
3. **Keep the controller a container.** Rejected: it is the exception to ADR 0188 that every other
|
||||
module's code has now left, and it costs a container runtime on the control machine and a
|
||||
container recreation in the middle of a plan.
|
||||
|
||||
## Decision
|
||||
|
||||
**Genesis raises the controller as a container, under the resource the controller's process
|
||||
`replaces`, and the first declaration the controller composes for its own machine hands it over.**
|
||||
The container is genesis's own shape, built from the controller's repository, and is recorded on the
|
||||
control machine exactly as the manifest's `replaces` names it, so the first apply after the pivot
|
||||
finds a replacement for it and removes it once the process is up. The controller's manifest declares
|
||||
only the process; the image form exists for genesis alone and is not a second way to run the
|
||||
controller on a live mesh.
|
||||
|
||||
This is the one bounded exception to ADR 0188 §1: a module's own code in an image, for the minutes
|
||||
between the pivot and the first push, on a mesh being created.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A new mesh ends where a running one is: the controller a process, no controller container.
|
||||
- Genesis keeps its steps; what changes is that it no longer reads the controller's container from the
|
||||
manifest, and that it records the container under the name the handover expects.
|
||||
- The controller's repository keeps its image build for genesis and the lab.
|
||||
- The handover is now on genesis's path too: a process that fails to stay up leaves the genesis
|
||||
container serving, and the apply says so — the same rule as on a live mesh.
|
||||
|
||||
## How it is checked
|
||||
|
||||
The installer's test raises a mesh whose controller manifest is the process form, and asserts that the
|
||||
container genesis recorded is exactly what the process `replaces`, so the first apply hands over and
|
||||
leaves one controller. Live, on the running mesh: after the manifest change is pushed, the control
|
||||
machine runs the controller as a process and no controller container, and the controller's seat
|
||||
answers throughout.
|
||||
|
||||
## References
|
||||
|
||||
- [Issue 213](../04-ISSUES/213-the-controller-is-a-go-program-run-in-a-container/00-report.md),
|
||||
[issue 223](../04-ISSUES/223-a-new-mesh-installs-its-controller-as-a-container/00-report.md)
|
||||
- mesh-host#86 (the handover), mesh-controller#252 (two controllers safe together),
|
||||
mesh-controller#253 (the controller's manifest as a process)
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||
---
|
||||
|
||||
# 201. A provider declares what it derives for each consumer, and the mesh tells both ends
|
||||
|
||||
> Written as 0188 on 2026-10-02 and renumbered to 0201 on 2026-10-04: the record of the bundles
|
||||
> refactor took 0188 on main while this one waited in a pull request, and the mesh's own code now
|
||||
> cites that one. Only the number moved; the decision is the one taken on the 2nd.
|
||||
|
||||
## Context
|
||||
|
||||
An arrangement between a consumer and a provider is delivered entirely by the mesh. Where the
|
||||
provider is, which port it answers on, what name the consumer must present, where its password
|
||||
is — each arrives as a fact the consumer reads from its binding, or as `${bound:…}` filled into a
|
||||
file before the declaration leaves the control plane. The provider invents none of it and hands
|
||||
none of it back ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)).
|
||||
|
||||
One kind of value escapes that. Where the **provider names the resource** — a bucket, a database,
|
||||
a vhost — the name is derived from the consumer, per consumer, and the mesh has no way to carry
|
||||
it. `serves` is a literal block in the provider's definition: the same values for every consumer.
|
||||
A provisioner's contract takes a provision and returns nothing. So a value the mesh's own rule
|
||||
produced reaches neither end as a statement; it is recomputed at one end and transcribed at the
|
||||
other.
|
||||
|
||||
The object store is the instance ([issue 124](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)).
|
||||
Its provisioner normalises the login the mesh minted into a bucket name and creates, checks and
|
||||
removes exactly that; the rule lives in twenty lines of the module's own TypeScript. Its three
|
||||
consumers each write the answer into their own definition by hand. Two transcribed it correctly;
|
||||
one named a predecessor's bucket, and would have authenticated successfully and been refused on
|
||||
every object, which reads like a credential fault and is not one.
|
||||
|
||||
Even corrected, the transcriptions are wrong in a second way. Each is `mesh-<node>-<slug>`, so
|
||||
each **names the machine the module happens to run on today** — a definition stating a fact about
|
||||
one installation, which [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||
forbids and whose check does not catch because the name is not a domain. Move any of the three to
|
||||
another machine and its configuration points at a bucket its key cannot open.
|
||||
|
||||
The shape is not the object store's. A database provisioner that prefixed names, a queue provider
|
||||
that scoped vhosts, any provider that derives a resource from who is asking: each forces the
|
||||
consumer to reproduce somebody else's rule and keep it in agreement by hand.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A served value may name the consumer the mesh is serving.** A `serves` block, which is
|
||||
literal today, may interpolate the mesh's own statement of who the consumer is:
|
||||
|
||||
- `${consumer:as}` — the identity the mesh minted for this consumer, exactly as the login it is
|
||||
told to present ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md));
|
||||
- `${consumer:as:dns}` — the same identity written as a DNS label.
|
||||
|
||||
Nothing else. **The mesh learns no protocol here; it spells its own name in an alphabet it already
|
||||
knows.** The identity is the mesh's, minted by the mesh, already capped at twenty characters
|
||||
because of what an S3 access key accepts; `dns` is that same name with its separator written `-`
|
||||
instead of `_`, which is the whole of the difference between the mesh's identifier alphabet and
|
||||
the one buckets, vhosts and hostnames use. A provider that needs a prefix or a suffix writes it
|
||||
around the placeholder, because a served value is a string.
|
||||
|
||||
The rejected alternative is **the provider returning values from provisioning** — the natural
|
||||
channel, since the provider is what derived them. It is rejected for three reasons, in order of
|
||||
weight. It inverts the delivery the mesh is built on: a grant would carry data the provider wrote
|
||||
rather than only data the mesh minted, and [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
|
||||
removed exactly that second path once already. It makes a consumer's declaration incomplete until
|
||||
its provider's reconcile loop has run, so a consumer could not be composed before a provider
|
||||
answered — a bootstrap order the mesh does not have and does not want. And it puts the rule where
|
||||
nothing can check it: a value that arrives from a running process cannot be refused at resolution,
|
||||
only discovered wrong later, which is the failure this record exists to end.
|
||||
|
||||
**2. The mesh resolves it once, per consumer, and tells both ends from the one resolution.** At the
|
||||
moment a consumer's declaration is composed, the mesh knows exactly who the consumer is. There, and
|
||||
only there, the placeholders are filled. The result reaches:
|
||||
|
||||
- the **consumer**, as the served facts in its binding file and as `${bound:<provision>:<key>}` in
|
||||
any file it writes — unchanged mechanisms, carrying one more key;
|
||||
- the **provider**, as `serves` on that consumer's entry in its contributions file, so the
|
||||
provisioner is *told* the name rather than recomputing it.
|
||||
|
||||
**The provider stops deriving in code and starts declaring.** One statement, filled once, delivered
|
||||
to both ends: the two cannot disagree, because there is no second computation to disagree with.
|
||||
|
||||
**3. A served value stays settled before it is per-consumer.** Settings still compose into `serves`
|
||||
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)), and the consumer
|
||||
placeholders are filled after that, so an operator may set a prefix and the mesh still derives the
|
||||
rest. A `${consumer:…}` naming a fact or an alphabet the mesh does not have is refused when the
|
||||
definition is parsed, with what it may say.
|
||||
|
||||
**4. A consumer may no longer name the resource its provider derives.** With the value delivered,
|
||||
a literal in a consumer's definition is not merely redundant — it is the one thing that can
|
||||
disagree with what the provider will actually create. The three object-store consumers lose their
|
||||
hand-written bucket names in this change.
|
||||
|
||||
**5. A consumer that keeps several holders of one provision may not be served a derived value.**
|
||||
Each holder gets its own login, `…_<local>` ([ADR 0094](0094-a-module-may-hold-several-secrets-from-one-provider.md)),
|
||||
and a provider derives from the login — so it would make one resource per holder, while the
|
||||
consumer's side has one binding and one `${bound:<provision>:<key>}`, both derived from the
|
||||
un-suffixed identity. That is this record's own failure one case to the side, and just as quiet:
|
||||
the consumer would authenticate and be refused on every object. Refused at resolution, naming
|
||||
both ends. Lifting it means giving the consumer's side a local dimension, which is a decision and
|
||||
not an omission.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One more thing a definition may say, and one less thing a module may be wrong about. The
|
||||
vocabulary grows by a placeholder; the catalogue loses three literals that named this
|
||||
installation's control node.
|
||||
- A provider's naming rule becomes readable in its definition instead of in its source. `minio`'s
|
||||
`bucketFor` goes; the manifest says `"bucket": "${consumer:as:dns}"` and the provisioner uses
|
||||
what it is given.
|
||||
- A provider that already serves consumers keeps serving them: the derived value equals what the
|
||||
code derived, so no bucket, database or login changes name. This is a change of **who says it**,
|
||||
not of **what is said**.
|
||||
- A refusal here fails **that machine's push**, naming the definition, and nothing else. That is
|
||||
deliberate and is the opposite of a module quietly left out: a definition that transcribes
|
||||
somebody else's rule is wrong everywhere, not just here, and the loud failure is in front of
|
||||
whoever can fix it.
|
||||
- The mesh now holds a rule in another system's alphabet — one rule, `dns`, stated once. A second
|
||||
alphabet is a decision, not an addition: the cost of each is that the mesh must be right about
|
||||
somebody else's naming, and that cost is only worth paying where the mesh already mints the name.
|
||||
|
||||
## How this is checked
|
||||
|
||||
- A served value naming an unknown fact or alphabet is refused at parse, with the list of what it
|
||||
may say — tested on both halves of the message.
|
||||
- Resolving a consumer whose provider derives a value puts that value in the consumer's binding
|
||||
file, in its `${bound:…}` substitutions, and in the provider's contributions entry for that
|
||||
consumer — one test asserting the three agree, because agreeing is the whole point.
|
||||
- Two consumers of one provider on one machine get two different derived values, and neither gets
|
||||
the other's.
|
||||
- A consumer with several holders of a deriving provider is refused, with both ends named — the
|
||||
test asserts the refusal, not merely that something failed.
|
||||
- A catalogue-wide test refuses a consumer definition that writes a literal where its provider
|
||||
derives: the provider's `serves` names the key, so the catalogue can say which definitions
|
||||
transcribe one.
|
||||
- `dns` is checked against the identity the mesh actually mints, not against an invented string:
|
||||
the test derives an identity with `ConsumerIdentity` and asserts the label it becomes.
|
||||
|
||||
## References
|
||||
|
||||
- [issue 124 — a consumer cannot be told a value its provider derived for it](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)
|
||||
- [ADR 0048 — a provider creates the credential the mesh minted, and seals nothing](0048-a-provider-creates-the-credential-the-mesh-minted.md)
|
||||
- [ADR 0049 — a consumer's identity fits the tightest backend](0049-a-consumers-identity-fits-the-tightest-backend.md)
|
||||
- [ADR 0174 — a node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||
- [ADR 0155 — a definition names no installation, and how that is checked](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||
- [design 27 — a module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
|
||||
@@ -188,7 +188,9 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0185** — [A control plane behind its seat's row serves what it can](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)
|
||||
- **0186** — [A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang](0186-a-ban-list-never-holds-a-neighbour.md)
|
||||
- **0187** — [A dead tracker is not the machine's failure](0187-a-dead-tracker-is-not-the-machines-failure.md)
|
||||
- **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md)
|
||||
- **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)
|
||||
- **0201** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0201-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -320,6 +322,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
||||
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
|
||||
- **0174** — [A node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||
- **0200** — [Genesis pivots to the controller as a container, and the first push hands it to a process](0200-genesis-pivots-to-the-controller-as-a-container-and-the-first-push-hands-it-to-a-process.md)
|
||||
|
||||
### How it is checked
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ code:
|
||||
- mesh-controller cmd/mesh-builder
|
||||
- mesh-controller internal/builder
|
||||
- mesh-catalog modules/build-agent
|
||||
updated: 2026-10-03
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md
|
||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||
@@ -317,6 +318,44 @@ build's lines reach a reader of its subject in order and the stream holds them a
|
||||
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
|
||||
after the roll-out read line by line through the console.
|
||||
|
||||
## The store keeps what the records name
|
||||
|
||||
*2026-10-02 — [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md),
|
||||
[issue 108](../../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md).*
|
||||
|
||||
Every build pushes another layer set and, until this, nothing ever removed one. The registry's own
|
||||
answer — collect what no tag names — is wrong for this mesh: each artifact is pushed under one
|
||||
moving tag and machines are pinned by digest, so every build but the newest is untagged and some
|
||||
machine may still be running it.
|
||||
|
||||
**The mesh decides and the store reclaims.** Deletion is enabled on the store's one door — that
|
||||
door already accepts a push, and a writer who can push can replace any tag, so delete takes
|
||||
nothing a push did not already have, and ADR 0082's bargain (a store every machine reaches with no
|
||||
credential to distribute first) is kept. The mesh then removes what it put there and no longer
|
||||
keeps, **naming it from its own build records** rather than enumerating the store: it has never
|
||||
put anything there it did not record, so a digest it did not record making is never named, which
|
||||
is what keeps the sweep away from the images genesis pushed before any record existed.
|
||||
|
||||
An artifact stays for one of two reasons and otherwise goes: a definition the mesh holds names it
|
||||
(no age limit — this is the floor), or it belongs to one of the five most recent successful builds
|
||||
of its module (somewhere for a wrong release to return to). The sweep runs after a build the mesh
|
||||
recorded, which is the moment new bytes landed and the moment the keep set moved; it needs no
|
||||
timer. Deleting a manifest frees no bytes, so the store's own collector runs nightly as a
|
||||
scheduled step with the server held still — which is what `while-stopped` exists for
|
||||
([design 20](20-writing-a-module.md)). Plain collection, not `--delete-untagged`: what the mesh
|
||||
keeps is still a manifest and so still referenced, and the dangerous flag is not needed once the
|
||||
mesh is the one deciding.
|
||||
|
||||
A machine behind by more than five builds of a module, recreating a container, cannot pull what it
|
||||
was running. It is already a machine the mesh reports as behind, and the answer is the current
|
||||
declaration.
|
||||
|
||||
*How it is checked:* the keep set, against records, holds what a manifest names and the five most
|
||||
recent builds and nothing else; a reference the mesh never recorded is never in the delete set; an
|
||||
image and an archive are asked for at their own endpoints; a store with deletion off names the
|
||||
remedy rather than the status code; a store that does not have it is recorded collected rather
|
||||
than retried for ever. Live: the store's size before and after the first nightly collection.
|
||||
|
||||
## The builder compiles the languages the mesh is written in
|
||||
|
||||
*2026-09-29 —
|
||||
|
||||
@@ -5,11 +5,12 @@ code:
|
||||
- mesh-catalog modules/showcase
|
||||
- mesh-controller internal/builder
|
||||
- mesh-sdk src
|
||||
updated: 2026-09-30
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
|
||||
- 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
|
||||
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||
- 02-DECISIONS/0040-what-a-module-is.md
|
||||
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
||||
@@ -208,3 +209,27 @@ is recreated with the new fact
|
||||
([ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md)). *How it is
|
||||
checked:* the host's unit tests run a step again when its named file changed and not otherwise,
|
||||
and recreate a container naming a step after the step ran.
|
||||
|
||||
## A recurring step may hold its own module's containers still
|
||||
|
||||
*Written 2026-10-02, from [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
|
||||
and [issue 108](../../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md).*
|
||||
|
||||
Some work cannot be done underneath a running service: an artifact store's collector walks the
|
||||
storage and requires every writer stopped. A `run-once` step runs *beside* containers and a
|
||||
scheduled one is the same container again, so until this a module had no way to say it — and the
|
||||
mesh inherited a store that has never collected anything, because the predecessor said it with a
|
||||
shell script and a script beside a module is not a resource in it.
|
||||
|
||||
A scheduled step may name `while-stopped`: resource ids of **its own module's** containers, which
|
||||
the host stops before the run and starts again after it, in the reverse order, **whatever the step
|
||||
did**. Three boundaries, each refused where it can be seen earliest — its own module's containers
|
||||
only, because a module that could quiesce a neighbour could stop the mesh; scheduled steps only,
|
||||
because at apply the declaration is applied in order and a step already gates what follows, so a
|
||||
one-time offline job says *before* rather than *instead of*; and restoring that is not conditional
|
||||
on anything, because the only real risk of the field is a window that never closes.
|
||||
|
||||
*How it is checked:* the host's unit tests assert stop–run–start in that order, the restart after a
|
||||
step that **failed**, the reverse order for several containers, and a service left down said
|
||||
loudly. The controller refuses, from the definition alone, a window with no schedule, one on a
|
||||
run-once step, one naming a container the module does not declare, and one naming itself.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller internal/catalogue]
|
||||
updated: 2026-09-30
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
|
||||
@@ -14,6 +14,7 @@ decisions:
|
||||
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
||||
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
|
||||
- 02-DECISIONS/0038-the-mesh-assigns-the-port.md
|
||||
- 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
---
|
||||
|
||||
# 27 — A module requires, the mesh resolves
|
||||
@@ -206,6 +207,22 @@ name when nothing sets it. That is the contract half of this design's operator p
|
||||
the placeholder allows: the definition says which values reach which requirement, and nothing else
|
||||
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
|
||||
|
||||
*A provider says once what it derives for each consumer (2026-10-02,
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md),
|
||||
[issue 124](../../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)):*
|
||||
where a provider **names the resource** it gives each consumer — a bucket, a database, a vhost — the
|
||||
name is derived per consumer, and a literal `serves` block could not carry it. A served value may
|
||||
now name the consumer the mesh is serving: `${consumer:as}`, the identity the mesh minted, and
|
||||
`${consumer:as:dns}`, that same identity written as a DNS label. Nothing else — **the mesh learns no
|
||||
protocol here; it spells its own name in an alphabet it already knows.** Settings are laid on first,
|
||||
so an operator may still set a prefix and the mesh derives the rest. The mesh fills it at the one
|
||||
moment it knows who the consumer is, and the one filled value reaches both ends: the consumer, as
|
||||
its binding's served facts and as `${bound:<provision>:<key>}` in any file it writes; the provider,
|
||||
as `derived` on that consumer's entry in its contributions file, so its provisioner is told the name
|
||||
rather than recomputing it. A consumer that writes the derived value into its own definition instead
|
||||
of asking for it is refused, naming the placeholder to use. *How it is checked:* the unit tests in
|
||||
ADR 0201's "how this is checked", each run against the unchanged controller first.
|
||||
|
||||
## How a definition reads what was resolved
|
||||
|
||||
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
||||
@@ -214,7 +231,9 @@ name in a configuration file writes the same thing: the requirement's name and t
|
||||
controller fills it at resolution.
|
||||
|
||||
This one form replaces the placeholders that exist today, one per mechanism: bound values, secrets,
|
||||
ports and machine facts.
|
||||
ports and machine facts. It subsumes the consumer placeholder too — a value a provider derives is
|
||||
read by the consumer exactly as any other field of the contract is, and `${consumer:…}` is only
|
||||
how the *provider* states the rule.
|
||||
|
||||
**The seat placeholder stays, for the controller alone.** The controller composes its own
|
||||
declaration and reaches the store and broker it made before any module existed, so it cannot be
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-tools, mesh-controller, mesh-host, mesh-catalog]
|
||||
updated: 2026-10-03
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||
@@ -309,6 +309,25 @@ nextcloud, minio), the two whose clients exist on no system (mongodb, mssql: a d
|
||||
and last the mesh's own (mesh-catalog, mesh-vault, records, gitea, mailu, audit-logger, lab, and the
|
||||
three mains).
|
||||
|
||||
*Built 2026-10-04.* Thirty-four modules no longer run their own code in a container: the first wave
|
||||
(mesh-catalog#245), the mesh's own and the media modules (mesh-catalog#248, mesh-media-catalog#13),
|
||||
with a step run where and as it is declared (mesh-host#85) and a process's words filled like a
|
||||
container's (mesh-controller#250). **Proven live** on every machine that runs them: each moved
|
||||
module's tools answer from the node's runtime, the steps run as their oneshot units, and the forge's
|
||||
merge events reach the build pipeline from the runtime — the merge after the move started its own
|
||||
plan. **Two corrections the machines taught:** a tool that called a broker's command-line client now
|
||||
runs it inside the broker's own container, because a host package may be uninstallable on a machine
|
||||
whose package index is stale (mesh-catalog#249); and a module reading its application's own key reads
|
||||
it through the application's container, because that directory belongs to the account the application
|
||||
runs as there, which is not the runtime's (mesh-media-catalog#14). *Completed 2026-10-04:* the last three — mesh-catalog, mongodb and mssql, whose code imports npm
|
||||
packages of its own — moved once the builder installs a bundle's own dependencies before compiling,
|
||||
keeping the toolchain's SDK authoritative (mesh-controller#255, mesh-catalog#250). Their database
|
||||
clients are now drivers inlined into the bundle, not command-line clients fetched by a container; one
|
||||
more correction the machines taught: a driver reaching its server on loopback must give TLS a host
|
||||
name, since the runtime's Node refuses an address (mesh-catalog#252). **No module's own code runs in
|
||||
a container any more;** every module with tools answers from its node's runtime, proven by calling a
|
||||
tool of each.
|
||||
|
||||
## WP5 — The shell, on a server first
|
||||
|
||||
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
||||
|
||||
+33
-4
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-controller internal/inventory, mesh-controller internal/artifacts, mesh-host internal/apply, mesh-catalog modules/distribution]
|
||||
fixed-by: 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
|
||||
---
|
||||
|
||||
# 108 — The registry has no garbage collection, and two doors make it harder to add
|
||||
@@ -75,3 +75,32 @@ real thing services need, and the mesh cannot express one.
|
||||
images by digest and moves by version — is retention "the digests no recorded build names"?
|
||||
- Who owns the routine when the store and its public door are two modules — the store, since the
|
||||
volume is its?
|
||||
|
||||
## Answered, 2026-10-02 — [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
|
||||
|
||||
The three open questions, answered:
|
||||
|
||||
- **A maintenance step, or a backend that does not need its writers stopped?** The step. A
|
||||
scheduled container may name `while-stopped` — resource ids of **its own module's** containers,
|
||||
which the host stops before the run and starts again after it whatever the step did. A storage
|
||||
backend the mesh does not run would be a bigger thing to own than the mechanism it avoids, and
|
||||
the mechanism is wanted anyway: a service that cannot have work done underneath it is a real
|
||||
shape and the mesh could not express it at all.
|
||||
- **Is retention "the digests no recorded build names"?** Nearly. An artifact stays because a
|
||||
definition the mesh holds names it (no age limit), or because it belongs to one of the five most
|
||||
recent successful builds of its module. Last-N-tags was the predecessor's rule for a registry
|
||||
that knew nothing else; this mesh knows what each digest is for.
|
||||
- **Who owns the routine now the second door is gone?** Both halves, each where it can be. The
|
||||
**mesh** decides what may go — only it holds the records — and asks the store to drop it. The
|
||||
**store** reclaims the bytes, because only it can stop its own server. Neither half can be done
|
||||
by the other.
|
||||
|
||||
And the sharpened point — enabling deletion on a door with no accounts — dissolved on inspection:
|
||||
**that door already accepts a push**, so a writer who can reach it can already replace any tag.
|
||||
Delete takes nothing a push did not have. What it does not do is undo ADR 0082's bargain, which
|
||||
putting an authenticated door in front of deletion would have.
|
||||
|
||||
The second registry process is not built, as the 2026-09-26 note says, so the shared blob cache
|
||||
and the deletion-cached-by-the-other-door problem never arise. Plain `garbage-collect` is enough:
|
||||
what the mesh keeps is still a manifest in the store, so `--delete-untagged` — the flag that would
|
||||
delete images machines are running — is not needed at all.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-sdk src/provisioner, mesh-catalog modules/minio]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-controller internal/catalogue, mesh-sdk src/provisioner, mesh-catalog modules/minio]
|
||||
fixed-by: 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
---
|
||||
|
||||
# 124 — A consumer cannot be told a value its provider derived for it, so it transcribes one
|
||||
@@ -63,3 +63,27 @@ compares it to what the provider will actually create. The one wrong instance wa
|
||||
- What would have caught the wrong instance? A test that resolves a consumer's grant and compares the
|
||||
bucket in its own configuration against the one the provider would create is a check that could
|
||||
exist today, for any interface, without the mechanism above.
|
||||
|
||||
## Answered, 2026-10-02 — [ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
|
||||
The channel is the provider's own `serves` block, which may now name the consumer the mesh is
|
||||
serving: `${consumer:as}` and `${consumer:as:dns}`. The mesh fills it once, where it knows who the
|
||||
consumer is, and delivers the one filled value to both ends — the consumer's binding and its
|
||||
`${bound:…}` substitutions, and the provider's contributions entry, so a provisioner is told the
|
||||
name rather than deriving it. Each open question above, answered:
|
||||
|
||||
- **Should a provider return values from provisioning?** No. It would make a grant carry data the
|
||||
provider wrote, make a consumer's declaration wait on its provider's reconcile loop, and put the
|
||||
rule where nothing can refuse it. The reasoning is in the record.
|
||||
- **Or should `serves` say a value is derived?** Yes, and the mesh performs the derivation — but it
|
||||
learns no protocol doing it. The only fact is the identity the mesh itself minted, in one of two
|
||||
alphabets it already knows.
|
||||
- **Should a consumer that names the resource be refused?** Yes. A consumer's file that already
|
||||
contains the value the mesh is about to derive for it is refused at resolution, naming the
|
||||
placeholder to write instead. That is the check this report asked for, and it is exact rather than
|
||||
heuristic: a derived value carries the identity minted for this consumer on this machine, which
|
||||
nothing else would spell out.
|
||||
|
||||
minio's `bucketFor` is gone; its manifest serves `"bucket": "${consumer:as:dns}"`. The three
|
||||
consumers' hand-written bucket names are gone with it — each of them also named the machine the
|
||||
module happens to run on, which is the second thing wrong with a transcription.
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-02
|
||||
located-in: [mesh-catalog modules/dnsmasq, mesh-controller cmd/mesh-controller]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 202 — A module whose required setting nobody set is left out of the machine, and the resolver is the module it happened to
|
||||
|
||||
## What was observed
|
||||
|
||||
Running the controller's own test suite against the catalogue beside it, 2026-10-02.
|
||||
`TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves` fails with *"the resolver
|
||||
was not handed the machines"*. Composing the same machine by hand and listing what it receives
|
||||
shows why: **dnsmasq contributes nothing at all.** Four resources are composed for that node, all
|
||||
of them the overlay's. The resolver's package, its configuration, its service and the fact that
|
||||
carries every machine's name are simply not there.
|
||||
|
||||
The cause is one line added to `dnsmasq`'s configuration earlier the same day: the addresses it
|
||||
listens on beside the machine's own became an operator setting,
|
||||
`listen-address=${setting:listen-addresses}`, with no default. A `${setting:…}` nothing sets is
|
||||
refused, a module that cannot be composed is **left out** rather than failing the whole machine
|
||||
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)), and so a node
|
||||
assigned the resolver is handed a declaration with no resolver in it.
|
||||
|
||||
The failing test is the symptom that surfaced it. The test is not what is wrong.
|
||||
|
||||
**Proven rather than inferred.** Composing the same machine a second time with
|
||||
`listen-addresses` set to `127.0.0.1` and nothing else changed, every one of dnsmasq's eight
|
||||
resources appears — `needs-broker`, `mesh-state`, `package`, `config`, `runtime-dns`, `runtime`,
|
||||
`service` and `fact-node-zones`. The only difference between a machine with a resolver and a
|
||||
machine without one is whether somebody set a value that did not exist yesterday.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
**Leaving a module out is right, and being quiet about it is not.** The rule exists so one
|
||||
module's broken setting cannot stop a machine converging — a good rule. But the outcome here is a
|
||||
machine that applies cleanly, reports current, and is missing its DNS resolver. Every name on that
|
||||
machine then resolves through whatever was there before, or not at all, and nothing in the mesh
|
||||
says the resolver was dropped. That is the shape
|
||||
[issue 152](../152-a-nodes-plan-failure-silently-drops-its-routed-names/00-report.md) records for
|
||||
routed names, here for a whole module.
|
||||
|
||||
**And a setting with no default is a definition that cannot be assigned.** Every other
|
||||
`${setting:…}` in the catalogue names something that is genuinely particular to one installation —
|
||||
a public domain, an issuer. "Which addresses besides my own do I answer on" has an obvious correct
|
||||
default for every machine that is not a LAN gateway: none beside loopback. A definition that
|
||||
refuses to compose until somebody sets a value most machines do not need is a definition that
|
||||
breaks the next node to be assigned it, and genesis with it.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
Whether the live machines are affected was not checked — those four have had the setting set, or
|
||||
their resolvers would already be gone. The claim is about a machine assigned the resolver *from
|
||||
now on*, and about the silence.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should the declaration say which modules it left out, where a person or the console can see it?
|
||||
`left_out` already travels to the host ([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md));
|
||||
what is missing is anything that reads it back and says so.
|
||||
- ~~Should a `${setting:…}` be allowed a default?~~ **Decided in principle and not built.**
|
||||
[ADR 0164](../../02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md)
|
||||
(proposed, 2026-10-01) says a setting with a default is a tunable and one without is the
|
||||
operator's, and narrows 0155's refusal to exactly the second. `listen-addresses` is a tunable by
|
||||
that rule, and dnsmasq declares no settings block at all. So this issue is, in part, 0164 waiting
|
||||
to be built — and in part the silence, which 0164 does not address.
|
||||
- Is leaving a module out ever right for a module a node is **assigned**, as opposed to one it
|
||||
merely pulls in? An assignment is somebody saying *this machine runs this*; silently not running
|
||||
it is the one answer nobody asked for.
|
||||
|
||||
## Still true on 2026-10-04, and the evidence had to be re-taken
|
||||
|
||||
Re-checked after the bundles refactor landed (fourteen records, ADRs 0188 and 0190–0200). **The
|
||||
fault stands and the old evidence no longer reaches it.**
|
||||
|
||||
`TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves` still fails on
|
||||
mesh-controller main, with the same message — and now for a *different first reason*. `assign` is
|
||||
refused before composition ever happens:
|
||||
|
||||
> dnsmasq on anchor has no bus credential: nothing was issued for anchor.dnsmasq … (novox/hq issue 203)
|
||||
|
||||
That is [issue 203](../203-a-fresh-assignment-is-pushed-before-its-credential-exists/00-report.md)'s
|
||||
new guard doing its job on a test harness that mints no credential. Two faults are stacked in one
|
||||
failing test, and the second was invisible behind the first.
|
||||
|
||||
Proven again, past both: mint `anchor.dnsmasq` so the assignment stands, then compose the machine
|
||||
twice. **Without `listen-addresses` the node composes four resources, all the overlay's. With it
|
||||
set to `127.0.0.1`, all eight of dnsmasq's appear** — `needs-broker`, `mesh-state`, `package`,
|
||||
`config`, `runtime-dns`, `runtime`, `service`, `fact-node-zones`. Nothing else differs.
|
||||
|
||||
The test is now wrong about two things and should be fixed with whichever of these is fixed first.
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
- mesh-controller#246
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -45,3 +46,7 @@ have to write — a bundle of language L depends on the module that publishes L'
|
||||
bundle lands in the tier after it. A test: a merge touching the toolchain module and a TypeScript
|
||||
bundle plans the bundle one tier later. Worked around on the day by building the bundle again once
|
||||
the toolchain was built.
|
||||
|
||||
## Resolved
|
||||
|
||||
Every mesh-tools plan since the fix ran in two rounds: the toolchain and runtime images first, what is built in or on them after. Before it, a merge moving both tiered them together. The planner's test proves the order: a merge moving a bundle and its toolchain plans two rounds.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-tools
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
- mesh-tools#44
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -42,3 +43,7 @@ install from a lockfile that a release updates, or build the install layer witho
|
||||
the build should record which SDK version the image carries, so a bundle's record says what it was
|
||||
compiled against. A check: after an SDK release and a toolchain rebuild, a bundle built on it reports
|
||||
the released version.
|
||||
|
||||
## Resolved
|
||||
|
||||
The toolchain now installs the exact SDK version the mesh last published, passed in as a build argument from the SDK module's published package, and the planner orders the toolchain after the SDK. Proven 2026-10-04: the toolchain built with the published SDK, and the six TypeScript bundles built in it serve their tools and seat verbs.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
- mesh-catalog
|
||||
- mesh-host
|
||||
fixed-by:
|
||||
- mesh-host#86
|
||||
- mesh-controller#252
|
||||
- mesh-controller#253
|
||||
- mesh-host#88
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -50,3 +54,27 @@ of a plan today.
|
||||
|
||||
Located only by owner; the move is a change of the controller's module and its deployment, not of
|
||||
its code.
|
||||
|
||||
## Fix prepared (2026-10-04)
|
||||
|
||||
Three changes. mesh-host#86, merged: a process may name the container it `replaces`, and the host
|
||||
removes that container only once the process has stayed up across two checks. mesh-controller#252,
|
||||
awaiting the operator's merge: the controller's composition for a service process, and two
|
||||
controllers safe together for the handover — the second stands by on the controller's consumers
|
||||
until the first lets go, and all plan work holds one advisory lock. mesh-controller#253, held: the
|
||||
controller's manifest as a Go bundle and a process. It waits on
|
||||
[issue 223](../223-a-new-mesh-installs-its-controller-as-a-container/00-report.md), because with it a
|
||||
new mesh cannot be installed.
|
||||
|
||||
## Resolved
|
||||
|
||||
Proven 2026-10-04 on the control machine: after the manifest change was merged and pushed, the host
|
||||
created the controller's account, ran its preparation step, started the controller as a process,
|
||||
found it up across both checks and removed the container. The controller now runs as its own account
|
||||
from its bundle, no controller container remains, its seat answered throughout, and the merge's own
|
||||
plan finished all three tiers under the new process.
|
||||
|
||||
One fault on the way, fixed before it could leave two controllers or none: the host read the account
|
||||
not existing yet as a user database that did not answer — it matched the exit as text in a wording
|
||||
its own runner did not use — so the first apply stopped at the account and the container kept serving,
|
||||
which is the handover's safe failure (mesh-host#88).
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
- mesh-controller#246
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -34,3 +35,7 @@ Owner mesh-controller (the planner). **Fix direction:** on start, and whenever a
|
||||
build, the plan settles an `asked` build against the build records — a build recorded as built from
|
||||
the plan's commit is that tier's outcome — so a plan resumes after the controller replaced itself.
|
||||
A test: a plan whose build outcome was recorded while no controller followed it resumes on start.
|
||||
|
||||
## Resolved
|
||||
|
||||
A plan settles an asked build from the build records, whoever heard the outcome. Proven 2026-10-03 and 2026-10-04: both controller merge plans since the fix finished all three rounds, including the round that replaced the controller, without being stopped by hand.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
- mesh-controller#246
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -33,3 +34,9 @@ Owner mesh-controller. **Fix direction:** a build asked at a commit does not cha
|
||||
module follows; or, if pinning is meant, the pin is said — in `module list`, in `status`, and by a
|
||||
merge's plan naming the module it leaves out and why. A test: building a module at a commit and then
|
||||
merging a change to it plans it.
|
||||
|
||||
## Resolved
|
||||
|
||||
Proven 2026-10-04: the catalogue merges since the fix rebuilt the module that had been pinned at an
|
||||
old commit, at the merge's commit, by an ordinary plan — the same as every other module of the
|
||||
catalogue. Nothing was asked for it by hand.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
- mesh-controller#246
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -28,3 +29,7 @@ Owner mesh-controller (the catalogue's registration check). **Fix direction:** a
|
||||
loads, runs or unpacks — no `loads`, no `tools` list on its module, no resource naming it — is refused
|
||||
at registration, naming the field that would deliver it. A test: such a manifest is refused; adding
|
||||
`loads` admits it.
|
||||
|
||||
## Resolved
|
||||
|
||||
A bundle nothing would deliver is refused at registration, by name. Proven by the controller's tests; the catalogue's bundles all name what the runtime loads (mesh-catalog#243).
|
||||
|
||||
+7
-1
@@ -1,9 +1,11 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-tools
|
||||
fixed-by:
|
||||
- mesh-tools#42
|
||||
- mesh-tools#43
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -50,3 +52,7 @@ announcement subscriptions are allowed for every principal kind.
|
||||
|
||||
Recovered on the day without the forge's API: the toolchain images built by the controller straight
|
||||
from the fix branch, every module image rebuilt on them, and the machine pushed.
|
||||
|
||||
## Resolved
|
||||
|
||||
A refused announcement is logged and the runtime serves on, and every runtime subscribes only the discovery subjects its grants allow. Proven 2026-10-04: after rolling out to every machine, no container restarts anywhere, and the discovery console lists no runtime as not answering. A refused tool subscription was made non-fatal the same way afterwards, under issue 218 (mesh-tools#46).
|
||||
|
||||
+29
-2
@@ -1,9 +1,13 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by: mesh-controller#248
|
||||
- mesh-tools
|
||||
fixed-by:
|
||||
- mesh-controller#248
|
||||
- mesh-tools#45
|
||||
- mesh-tools#46
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -63,3 +67,26 @@ and memberships come from the same list, so they cannot disagree.
|
||||
seats and loses the recorded mesh seat. Live, the discovery console's overview must show each
|
||||
mesh-scoped seat announced from exactly the holder the records name. Status moves to `resolved` once
|
||||
that holds after the fix is rolled out.
|
||||
|
||||
## A second cause, and what the rollout broke (2026-10-04)
|
||||
|
||||
With the grants corrected, calls to the store reached only the holder, yet the console still showed
|
||||
the seat announced from both machines. The module's runtime added every seat its start-up credential
|
||||
claims, even after the mesh issued a membership that left the seat out. Once a membership exists,
|
||||
it now alone decides which seat verbs a runtime serves (mesh-tools#45).
|
||||
|
||||
The rollout then exposed a third fault. The module on the machine that does not hold the seat was
|
||||
still running an image built before #45, so it subscribed to the seat's subject. The corrected grants
|
||||
refused that subscription, and the refusal ended the process. Its runtime crash-looped until the
|
||||
module was rebuilt on the new runtime image. The database itself kept running. A refused tool
|
||||
subscription is now logged and costs only that subject (mesh-tools#46), as a refused announcement
|
||||
already did ([issue 217](../217-a-refused-announcement-took-down-every-containers-runtime/00-report.md)).
|
||||
|
||||
The module was not rebuilt by the plan that rebuilt the runtime image. This is the ordering gap of
|
||||
[issue 211](../211-a-bundle-is-built-before-the-toolchain-it-is-compiled-in/00-report.md) seen
|
||||
from a container module.
|
||||
|
||||
**Proven 2026-10-04.** The discovery console's overview shows the store seat announced from the
|
||||
recorded holder only. Repeated calls to the store are answered by that machine, and the answers
|
||||
include the controller's own database. The non-holder still answers its own module tools. No
|
||||
container restarts on any machine.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
- mesh-controller#249
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 219 — An older build that finishes later replaces a newer one
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-03. Two merges to the runtime module came minutes apart. Each plan asked for every module
|
||||
built on the runtime image to be rebuilt. One module's two builds were of the same source and
|
||||
differed only in the runtime image they stood on:
|
||||
|
||||
| Build | Requested | Finished | Stood on |
|
||||
|---|---|---|---|
|
||||
| asked by the first plan | 21:33 | 22:04 | the runtime image before the fix |
|
||||
| asked by the second plan | 21:49 | 21:56 | the runtime image with the fix |
|
||||
|
||||
The older request finished last, and its image became the module's current artifact. The next push
|
||||
deployed it, and the module's runtime crash-looped on a fault the newer image had already fixed
|
||||
([issue 218](../218-a-mesh-seat-is-answered-by-a-module-that-does-not-hold-it/00-report.md)).
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
Which build is current should follow what it was built from, not which build machine was slowest.
|
||||
Whenever two plans overlap, which happens on any busy evening, a fix can be silently reverted by a
|
||||
build that started before it existed. Every passing check still passes.
|
||||
|
||||
## Where to look
|
||||
|
||||
How a finished build is recorded and how a module's current artifact is chosen. **How it is checked:**
|
||||
a test in which an older request completes after a newer one for the same artifact, and the newer
|
||||
stays current.
|
||||
|
||||
## Resolved
|
||||
|
||||
A build is ordered by when it was requested, read from the id the controller gives it, and a
|
||||
module's registered manifest is replaced only by a build requested at or after the one it came from.
|
||||
An older request finishing later is recorded and changes nothing; a plan settles only from builds it
|
||||
asked for itself. **How it is checked:** store-backed tests replay the incident — the newer request
|
||||
stays what the module is — and fail without the fix. Live since 2026-10-04: the rebuilds of every
|
||||
runtime-image module and the three waves of module code moves since then each registered the build
|
||||
they asked for.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-host
|
||||
fixed-by:
|
||||
- mesh-host#84
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 220 — A delivered bundle keeps the files of the one before
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04. A tools bundle was rebuilt as one self-contained file per entrypoint
|
||||
([ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md)),
|
||||
so it no longer carries a package directory. On the machine it was delivered to, its directory still
|
||||
held the package directory and a compiled file from the earlier delivery, dated hours before the
|
||||
new files. The new files were written over the old directory, and nothing removed what the new
|
||||
bundle no longer contains.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
A bundle on disk should be exactly the artifact that was built. Leftover files can be imported by
|
||||
code that should no longer find them. A fix that removes a file then works on a fresh machine and
|
||||
fails on every machine that ran an earlier version. It also makes "what runs here" impossible to
|
||||
read from the artifact.
|
||||
|
||||
## Where to look
|
||||
|
||||
How the host unpacks a bundle into its directory. **How it is checked:** deliver a bundle, then a
|
||||
version without one of its files, and the file is gone.
|
||||
|
||||
## Resolved
|
||||
|
||||
An archive is unpacked into a fresh directory beside the old one and swapped in by rename; a refused
|
||||
or failed unpack leaves the old tree whole. **How it is checked:** a second delivery without a file
|
||||
removes it, and nothing is left beside the directory; both tests fail without the fix. Proven
|
||||
2026-10-04: a bundle rebuilt and delivered after the fix holds exactly the new build and nothing
|
||||
beside it. A bundle that has not changed since keeps its old leftovers until its next version, by
|
||||
design: an unchanged archive is not unpacked again.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
- policy: `upgrade build-agent roll-out` (2026-10-04)
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 221 — A build machine learns a new builder only from a push
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04. A controller merge changed how TypeScript bundles are built: one file per entrypoint
|
||||
instead of a package directory. Its plan finished, and six bundles were rebuilt right after. They
|
||||
came out in the old shape, because the build machines still ran the previous builder. They got the
|
||||
new one only from the next push. Rebuilt after that push, the same six came out right.
|
||||
|
||||
The same order showed in the bus grants the same night. A push sent while the controller was still
|
||||
the previous build composed grants with the previous code. A second push was needed after the new
|
||||
controller had started.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
A merge to the controller is not in effect when its plan says done. Anything built or pushed in the
|
||||
gap uses the old code and looks current. Today only the operator knows to push first and build
|
||||
second, and even the operator forgot.
|
||||
|
||||
## Where to look
|
||||
|
||||
Whether a controller plan should end by delivering itself to the build machines and the control
|
||||
machine, or whether a build should refuse a builder older than the controller that asked for it.
|
||||
**How it is checked:** after a controller merge, a build asked right after its plan finishes runs
|
||||
the new builder.
|
||||
|
||||
## Located
|
||||
|
||||
The mechanism existed: a module whose upgrade policy is `roll-out` is sent to its machines when its
|
||||
tier is built, and the plan's next tier waits until it is applied. The build agent's policy was
|
||||
`record` — built, never sent — as was that of 76 other modules, which is why every rollout on
|
||||
2026-10-03 needed a push by hand. The build agent was set to `roll-out`, one machine at a time,
|
||||
stopping at the first failure. **How it is checked:** the next controller merge's plan sends the
|
||||
build agent to the build machines before its last tier, and a build asked right after the plan
|
||||
finishes runs the new builder; then this moves to resolved. Whether the other modules roll out is
|
||||
the operator's policy, not this issue's.
|
||||
|
||||
## Resolved
|
||||
|
||||
Proven 2026-10-04: the next controller merge's plan logged that its first tier was built and the
|
||||
build agent sent to all four build machines, and only then asked its next tier. The builder change
|
||||
in that merge reached the build machines without a hand push.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-tools
|
||||
fixed-by:
|
||||
- mesh-tools#47
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 222 — An assignment is refused on the bus until the bus's machine is pushed
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04. A module was assigned to the laptop, and the laptop alone was pushed. The module's two
|
||||
bundles arrived and the node's runtime launched both, and then the bus refused every one of the
|
||||
module's subjects:
|
||||
|
||||
```
|
||||
nats: permissions violation: Permissions Violation for Subscription to "mesh.mod.<module>.tool.<tool>.<node>"
|
||||
```
|
||||
|
||||
The tools were unreachable until a later push that included the machine running the bus. Then the
|
||||
runtime served them without a restart, because a new membership arrived and it re-subscribed.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
What an account may answer lives in the bus's user list, and the controller writes that list only
|
||||
into the declaration of the machine that runs the bus. Assigning anything to any machine changes
|
||||
that list, so `push <machine>` after `assign <machine> <module>`, which is what the controller itself
|
||||
tells the operator to run, leaves the module running and unreachable, with nothing reporting a fault.
|
||||
|
||||
## Where to look
|
||||
|
||||
Whether a push to one machine should also send the bus's machine when the user list it would
|
||||
compose differs from the one that machine holds. **How it is checked:** assign a module with tools
|
||||
to a machine that does not run the bus, push only that machine, and its tools answer.
|
||||
|
||||
## Diagnosed and resolved
|
||||
|
||||
The push did send the bus's machine: the controller's log shows both machines applying in the same
|
||||
second. The fault was the order within that second. The node's runtime subscribed before the bus
|
||||
had reloaded its user list, the bus refused, and the bus client marks a refused subscription dead.
|
||||
Nothing asked again until a later membership happened to re-serve the module.
|
||||
|
||||
A subject the runtime answers on is now asked for again when the bus refuses it, after waits from
|
||||
two seconds to two minutes, and given up and said after about five minutes. **How it is checked:**
|
||||
a test refuses a subject and finds it asked for again and answering, given up past its attempts,
|
||||
and not asked again once stopped; and by hand against a bus whose permissions were reloaded while
|
||||
connected, the runtime answered two seconds after the grant arrived, where the runtime before the
|
||||
fix never answered.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-host
|
||||
fixed-by:
|
||||
- mesh-host#87
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 223 — A new mesh installs its controller as a container
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04. Preparing [issue 213](../213-the-controller-is-a-go-program-run-in-a-container/00-report.md),
|
||||
the controller's own manifest was changed to a Go bundle run as a process. The installer that raises a
|
||||
new mesh assumes the controller is an image and a container at every step from its third on:
|
||||
|
||||
- it requires the controller's build to produce exactly one image;
|
||||
- it starts a temporary controller from that image, and publishes the image to the registry;
|
||||
- at the pivot, it finds the controller's container in the declaration, reads its environment and
|
||||
volumes, waits for it, and from then on talks to the controller only through the container.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
With the controller's manifest changed, a new mesh cannot be installed: the pivot fails. The deeper
|
||||
constraint is ordering. A process's bundle is fetched from the artifact store, and the installer
|
||||
raises the artifact store only after the pivot, so the controller's first declaration names a bundle
|
||||
nothing can serve yet.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
One of two shapes, and it is a decision, not a repair:
|
||||
|
||||
1. raise the artifact store before the pivot, publish the controller's bundle to it, and talk to the
|
||||
controller from the host's side rather than through a container; or
|
||||
2. pivot to the image form as today, and let the first push hand over to the process, which
|
||||
requires the controller's manifest to carry both forms.
|
||||
|
||||
Until it is settled, the change of the controller's manifest (mesh-controller#253) is held. The
|
||||
handover itself is built and merged (mesh-host#86); the controller's half (mesh-controller#252) waits
|
||||
on the operator. **How it is checked:** the installer's test raises a mesh whose controller manifest
|
||||
is the process form, and the controller answers its seat's verbs at the end.
|
||||
|
||||
## Decided (2026-10-04)
|
||||
|
||||
Option 2, [ADR 0200](../../02-DECISIONS/0200-genesis-pivots-to-the-controller-as-a-container-and-the-first-push-hands-it-to-a-process.md):
|
||||
genesis pivots to the controller as a container recorded under the name the process `replaces`, and
|
||||
the first push hands it over.
|
||||
|
||||
## Resolved
|
||||
|
||||
Built as [ADR 0200](../../02-DECISIONS/0200-genesis-pivots-to-the-controller-as-a-container-and-the-first-push-hands-it-to-a-process.md)
|
||||
decided: genesis reads only the controller's process from the manifest, builds the controller's image
|
||||
from its repository, and pivots to a container of its own shape recorded under the id the process
|
||||
`replaces`; an older controller in the image form still builds as before. **How it is checked:** the
|
||||
installer's tests build the genesis form from the controller's real manifest, and apply the
|
||||
controller's first process declaration over the recorded container with the host's own apply — the
|
||||
process starts, the container is removed, one controller remains. A real install from nothing has not
|
||||
been run since; the handover it ends in was proven live on the running mesh (issue 213).
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-host internal/apply/apply.go, mesh-host internal/apply/schedule.go]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 224 — An apply arriving during a maintenance window reopens it, by recreating the container the window is holding still
|
||||
|
||||
## What was observed
|
||||
|
||||
Reviewing [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)'s
|
||||
`while-stopped` before merging it, 2026-10-04. Found by reading, not by running.
|
||||
|
||||
A scheduled step may hold its module's containers still while it runs. The host stops them, runs
|
||||
the step, starts them again. Nothing tells the **apply** that a window is open, and the apply's
|
||||
rule for a container it finds stopped is to replace it:
|
||||
|
||||
```
|
||||
case existed && (before.Spec == want || legacy) && before.Running && len(reasons) == 0:
|
||||
out.Action = "unchanged"
|
||||
case existed:
|
||||
rm -f
|
||||
```
|
||||
|
||||
`before.Running` is false for a container a window is holding, so the second branch takes it:
|
||||
the container is removed and recreated, **running**, in the middle of the step that required it
|
||||
to be still.
|
||||
|
||||
## Why it matters
|
||||
|
||||
For the store, which is what the field was built for, the chain is: a push lands at 03:30 → the
|
||||
apply recreates the registry → the registry accepts an upload from a build running at the same
|
||||
time → `garbage-collect`, already past its mark phase, sweeps the blob that upload just wrote.
|
||||
The image is then in the store with a layer missing, and the build that made it reported success.
|
||||
|
||||
Two things have to coincide, so it is not likely. It is also not rare enough to leave unsaid: the
|
||||
mesh pushes on every merge, at any hour, and a collection over a store this size is minutes rather
|
||||
than seconds.
|
||||
|
||||
**The general shape is the one that matters.** `while-stopped` is the first thing in the mesh that
|
||||
makes a container's stopped state *intentional*. Everything else in the host reads "stopped" as
|
||||
"broken, fix it", which is right everywhere else and wrong here. Any future use of the field
|
||||
inherits this.
|
||||
|
||||
## What this is not
|
||||
|
||||
Not a regression. The store has never collected anything, so nothing is worse than it was; this
|
||||
is a hole in something new rather than something that broke.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Should a window take the apply lock?** The daemon already serialises applies with `applying`
|
||||
and, across processes, with `store.Lock`. A window that held it would make the race impossible.
|
||||
The cost is that a push arriving mid-window waits for minutes, and a push that waits is what
|
||||
[issue 185](../185-a-refused-membership-publish-stops-the-controller/00-report.md)'s
|
||||
family of outages looked like from outside.
|
||||
- **Or should the apply learn that a container is held?** Narrower: the scheduler says which
|
||||
containers a window currently holds, and `applyContainer` reports those unchanged instead of
|
||||
recreating them. Nothing blocks, and the apply tells the truth for the minutes it matters —
|
||||
at the cost of a second source for "is this container meant to be running".
|
||||
- Either way: should the *report* say a window is open, so a machine that looks half-stopped at
|
||||
03:31 reads as working rather than broken?
|
||||
Reference in New Issue
Block a user