Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a31cfcf461 | ||
|
|
75b3861911 | ||
|
|
694555214a | ||
|
|
a7249541df | ||
|
|
95d8253f71 |
@@ -4,12 +4,15 @@ status: implemented
|
||||
code:
|
||||
- mesh-controller internal/catalogue/seats.go
|
||||
- mesh-controller internal/catalogue/resolve.go
|
||||
- mesh-controller internal/inventory/seats.go
|
||||
- mesh-controller internal/inventory/migrations/0039-a-seat-is-held-by-one-assignment-on-record.sql
|
||||
- mesh-controller cmd/mesh-controller/seats.go
|
||||
- mesh-controller cmd/mesh-controller/source.go
|
||||
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
||||
- mesh-catalog modules/gitea/module.json
|
||||
updated: 2026-09-27
|
||||
decisions:
|
||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||
@@ -42,6 +45,31 @@ is refused. A seat makes a role singular, never a module.
|
||||
**The seat points at the assignment.** Everything the mesh knows about the holder is what it knows
|
||||
about that assignment: the node, the node's settings for the module, and what the module serves.
|
||||
|
||||
**Which assignment holds a seat is a fact on record, and changes as one act.** Revision, 2026-09-27
|
||||
([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)). Until
|
||||
then the holder was derived — the module that is assigned and claims the seat holds it, and a second
|
||||
eligible assignment was refused. That has no way to pass a seat from one holder to the next without a
|
||||
moment in which nobody holds it, and the controller finds its own bus through one of these seats: that
|
||||
moment took the control plane down for an evening. So the holder is now one row the controller keeps,
|
||||
written by a handover — `seat <name> --to <node>/<module>` — that names the seat and the assignment
|
||||
taking it over and replaces the previous holder in the same write. Between two handovers the seat has
|
||||
exactly one holder, and it is never none.
|
||||
|
||||
Three consequences follow. **A seat with no row is held as it always was**: the sole eligible
|
||||
assignment holds it, and two eligible ones are refused — so a mesh that has never handed a seat over
|
||||
behaves exactly as before, and the row appears the first time somebody does. **With a row, any other
|
||||
assignment whose module could hold the seat is eligible and silent**: neither refused nor holding.
|
||||
That is what lets the next holder run beside the current one until the handover, which the bus's move
|
||||
needs ([28](28-building-the-bus.md), task 5.3). **And a holding is the assignment's**: unassigning the
|
||||
holder takes the row with it, so a seat never points at something that is not running anywhere, and
|
||||
the seat falls back to derivation rather than to nothing.
|
||||
|
||||
The handover refuses what would make the new holder wrong before anything is written: the seat must
|
||||
exist, the assignment must exist, and the module must be able to hold the seat — claim it at its scope
|
||||
and provide what it delivers, judged against the store's row and not against anything compiled into a
|
||||
binary. It does not check that the module is running yet; `push` confirms that afterwards, and a
|
||||
handover that could only be recorded after the new holder was up could not be the switch.
|
||||
|
||||
**The set is closed.** A seat the mesh does not define is refused wherever it is named, and so is one
|
||||
named at the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the
|
||||
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
|
||||
@@ -207,7 +235,9 @@ checked as their tables say:
|
||||
| `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. |
|
||||
| Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. |
|
||||
| A holder satisfies the seat's protocol | 0118: a claim whose module does not serve what the seat declares is refused at assignment. |
|
||||
| A seat is held by one assignment, and only by one whose module can hold it | 0118: resolution tests for a second holder and for a seat the definition does not name. |
|
||||
| A seat is held by one assignment, and only by one whose module can hold it | 0118: resolution tests for a second holder and for a seat the definition does not name. 0131: `CanHold` is the one judgement, shared by registration and the handover, and its test follows the store's row. |
|
||||
| A holder on record settles the seat; another eligible assignment is silent, not refused | 0131: resolution tests with a recorded holder on the same machine, on another machine, and under a seat's former name; without a record, the old rule's tests still pass unchanged. |
|
||||
| A handover replaces the holder as one write, needs an assignment to point at, and goes with it | 0131: store tests — a second handover leaves one row; a handover to a module not assigned where named is refused; unassigning the holder removes the row. |
|
||||
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
|
||||
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
|
||||
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
|
||||
|
||||
@@ -535,7 +535,7 @@ it, and the beds that need a mesh living on NATS can finally run.
|
||||
The outcome carries the module name, because only the manifest says what was built and one
|
||||
message now has three readers. A failed build names none: it produced no module version, and
|
||||
the catalogue would otherwise place something that was never made.
|
||||
- [~] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
|
||||
- [x] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
|
||||
**the installer can raise it**: a foundation template that stands up the server, writes the
|
||||
server's own settings and the mesh's first user list beside them, and starts a controller
|
||||
reaching the new bus. What remains is running it, which is 4.1's bed.
|
||||
@@ -655,42 +655,35 @@ healthy while reacting to nothing.
|
||||
- [ ] 5.1 the cutover bed: a mesh on AMQP with a predecessor stand-in on the deprecated broker
|
||||
moves its bus in one rollout, every node reporting on NATS afterwards, the stand-in's own
|
||||
client still connected throughout
|
||||
- [~] 5.2 the rollout: accounts composed, then the controller, every host and every runtime
|
||||
- [x] 5.2 the rollout: accounts composed, then the controller, every host and every runtime
|
||||
together; every node confirmed heard before AMQP stops.
|
||||
|
||||
**The readiness half is in and is the half worth having.** The move takes every node at once, so
|
||||
there is nothing to inspect afterwards and no half to roll back — either the mesh was ready or it
|
||||
was not. `rollout check` answers that from records, with one dial: is a bus answering, does a
|
||||
machine hold the seat, has it been sent the composed user list, does every machine and every
|
||||
module that speaks have a credential. Each missing thing names its own next step, because "not
|
||||
ready" that cannot be acted on is not an answer at the point where the next step is irreversible.
|
||||
**Done 2026-09-28, 02:25.** Every machine reports on the new bus, the seat is held by the
|
||||
module that provides it, the old broker is unassigned and forgotten, and every credential was
|
||||
minted afresh at the end because two had been printed on the way. What it took, in the order
|
||||
it was found, each fixed on the trunk before the next step: the control plane's `serve` and
|
||||
`push` never selected the new transport (task 4.3, open until then); a machine's user was
|
||||
granted neither the asking nor the delivery of its own consumer; the account had no JetStream
|
||||
of its own; the control plane's client verified the bus's certificate by name instead of
|
||||
pinning it; the seat table's rows carried no protocol, so no role's work queue was raised; the
|
||||
build machine decided its bus from a variable its container never received; and a rotation
|
||||
put new hashes on the bus before three machines had received their new memberships — which
|
||||
is why there is now `rollout hand <node>` and a host adopts a delivered membership at start.
|
||||
The bootstrap loop — a bus that can only be raised by a declaration that can only arrive
|
||||
over that bus — was broken once, by hand: the mesh's own composed configuration started the
|
||||
server, and the controller binary was run on the node directly until the managed container
|
||||
could be rebuilt over the bus it was on.
|
||||
|
||||
**A machine with no credential is what must stop it.** It keeps running, cannot come back, and
|
||||
afterwards there is no bus to tell it anything over.
|
||||
|
||||
The move itself is deliberately not written yet, and the command says so rather than pretending:
|
||||
it waits on the check having been run against a real mesh. Writing the irreversible half before
|
||||
the question it depends on has ever been asked of something real is how the plan's own rule about
|
||||
beds gets broken by another route.
|
||||
|
||||
> **What this costs if it goes wrong, measured rather than assumed.** Nothing in a served
|
||||
> request's path goes over the mesh's own bus: modules serve from their own containers. What a
|
||||
> failed move costs is the mesh's ability to *change* anything — pushes, tool calls, new
|
||||
> provisioning — until it is finished or undone. That is worth knowing before rather than
|
||||
> after, and it is why the operator's "as long as my services keep running" is a reasonable
|
||||
> position rather than a gamble. **Measured on 2026-09-27**, when a seat emptied itself
|
||||
> mid-change: 52 containers stayed up and the broker never stopped; the control plane
|
||||
> crash-looped for two hours and nothing could be deployed until it was repaired by hand.
|
||||
> An earlier version of this note said the old broker stays as an ordinary provider of
|
||||
> `amqp` ([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)); that is withdrawn by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) — see 5.4.
|
||||
- [ ] 5.3 **the seat changes hands as one act.** A command takes a seat and the assignment taking it
|
||||
- [x] 5.3 **the seat changes hands as one act.** A command takes a seat and the assignment taking it
|
||||
over, and the seat is never empty in between — the emptiness is the outage of 2026-09-27, when
|
||||
the control plane, which finds its own bus through this seat, lost the address and looped.
|
||||
Today only `seat rename` exists. This is what 5.2 uses to move `mesh-broker` from the old
|
||||
**Built 2026-09-27** (`seat_holding`, migration 0039; design 26 says how it is checked), and used
|
||||
live the next night to hand `mesh-broker` from the old broker's assignment to the new one's. This
|
||||
is what 5.2 uses to move `mesh-broker` from the old
|
||||
broker's assignment to the new one's, and it is built first ([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)).
|
||||
- [ ] 5.4 **the old broker and everything that named AMQP leave the mesh** ([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md),
|
||||
- [x] 5.4 **the old broker and everything that named AMQP leave the mesh** ([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md),
|
||||
superseding [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)): the two modules that
|
||||
required `amqp` are removed, the broker's module is unassigned and removed, registration refuses
|
||||
required `amqp` are removed, the broker's module is unassigned and removed (**done 2026-09-28**; the predecessor's own tooling, which rode the same adopted broker, went dark with it, as [ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md) accepted), registration refuses
|
||||
a manifest that provides or requires `amqp`, and a whole-catalogue check asserts none does. Not
|
||||
a retirement condition — a decision, taken, with the operator's "I don't care if the predecessor
|
||||
breaks" on record ([ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)).
|
||||
|
||||
Reference in New Issue
Block a user