Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6402cf777 | ||
|
|
4f0d144833 | ||
|
|
98d94ef71e | ||
|
|
4e13280604 | ||
|
|
8783a13448 | ||
|
|
a31cfcf461 | ||
|
|
75b3861911 | ||
|
|
694555214a | ||
|
|
a7249541df | ||
|
|
95d8253f71 | ||
|
|
784b487bf9 | ||
|
|
7ae711ba0b |
+1
-1
@@ -40,7 +40,7 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It
|
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It
|
||||||
keeps running as an **ordinary provider** of the `amqp` provision, for modules that need a
|
keeps running as an **ordinary provider** of the `amqp` provision, for modules that need a
|
||||||
message broker of their own the way something needs a database
|
message broker of their own the way something needs a database
|
||||||
([ADR 0127](../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)) — no seat, not foundation,
|
([ADR 0127](../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md))) — no seat, not foundation,
|
||||||
never raised at genesis, and a mesh that never installs it is complete.
|
never raised at genesis, and a mesh that never installs it is complete.
|
||||||
|
|
||||||
Say *the deprecated broker*, not "the compatibility broker" (it serves the mesh's own modules,
|
Say *the deprecated broker*, not "the compatibility broker" (it serves the mesh's own modules,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
topic: the mesh
|
topic: the mesh
|
||||||
status: accepted
|
status: superseded
|
||||||
|
superseded-by: 0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
date: 2026-09-26
|
date: 2026-09-26
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ status: accepted
|
|||||||
date: 2026-09-26
|
date: 2026-09-26
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
extends: 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
extends: 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 128. The mesh bus is required, not ambient
|
# 128. The mesh bus is required, not ambient
|
||||||
|
|
||||||
|
|
||||||
|
> **Pointer repointed, 2026-09-27.** This record was written extending
|
||||||
|
> [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)), which
|
||||||
|
> [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) has since superseded — AMQP is
|
||||||
|
> not a provision at all. Nothing decided here changes; the frontmatter now rests on the live record,
|
||||||
|
> and the citations below are read with that in mind.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[Design 29](../03-DESIGN/01-to-be/32-what-a-module-declares.md) opened by saying the bus is
|
[Design 29](../03-DESIGN/01-to-be/32-what-a-module-declares.md) opened by saying the bus is
|
||||||
@@ -72,7 +79,7 @@ process in the path and nothing waiting on a bus account to create bus accounts.
|
|||||||
whose provider is the mesh itself.
|
whose provider is the mesh itself.
|
||||||
|
|
||||||
**A module may also provide a NATS server of its own, and that is a different interface.** Exactly
|
**A module may also provide a NATS server of its own, and that is a different interface.** Exactly
|
||||||
as the AMQP broker provides `amqp` ([ADR 0127](0127-amqp-is-a-provision-not-the-bus.md)), a module
|
as the AMQP broker provides `amqp` ([ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md))), a module
|
||||||
may run its own NATS and offer it as a backing service. That interface is **`nats`**; the mesh's
|
may run its own NATS and offer it as a backing service. That interface is **`nats`**; the mesh's
|
||||||
own bus is **`mesh-bus`**; the two are never the same name, because a manifest that said `nats`
|
own bus is **`mesh-bus`**; the two are never the same name, because a manifest that said `nats`
|
||||||
could mean either and the difference is the whole architecture. The rule from 0119 decides which
|
could mean either and the difference is the whole architecture. The rule from 0119 decides which
|
||||||
@@ -109,7 +116,7 @@ is legitimate: a private bus is a backing service, never a channel to another mo
|
|||||||
|
|
||||||
- [ADR 0125](0125-the-bus-is-the-only-broker.md) — superseded by 0119; its bootstrap argument is
|
- [ADR 0125](0125-the-bus-is-the-only-broker.md) — superseded by 0119; its bootstrap argument is
|
||||||
narrowed here to the case it supports.
|
narrowed here to the case it supports.
|
||||||
- [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) — a broker as a backing service; this
|
- [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) — a broker as a backing service; this
|
||||||
applies the same shape to the mesh's own bus and separates the two names.
|
applies the same shape to the mesh's own bus and separates the two names.
|
||||||
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — authority from
|
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — authority from
|
||||||
declarations, which this leaves untouched.
|
declarations, which this leaves untouched.
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ status: accepted
|
|||||||
date: 2026-09-27
|
date: 2026-09-27
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
extends: 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
extends: 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 130. The predecessor is ending, and its broker goes with it
|
# 130. The predecessor is ending, and its broker goes with it
|
||||||
|
|
||||||
|
|
||||||
|
> **Pointer repointed, 2026-09-27.** This record was written extending
|
||||||
|
> [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)), which
|
||||||
|
> [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) has since superseded — AMQP is
|
||||||
|
> not a provision at all. Nothing decided here changes; the frontmatter now rests on the live record,
|
||||||
|
> and the citations below are read with that in mind.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) settled that the old broker is an ordinary
|
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) settled that the old broker is an ordinary
|
||||||
provider of the `amqp` provision rather than a compatibility module with an end date. It rejected
|
provider of the `amqp` provision rather than a compatibility module with an end date. It rejected
|
||||||
giving it a retirement condition, and said why: *"its clients are not only the predecessor's, so the
|
giving it a retirement condition, and said why: *"its clients are not only the predecessor's, so the
|
||||||
retirement condition describes a day that will not come."*
|
retirement condition describes a day that will not come."*
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-27
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
supersedes: 0127-amqp-is-a-provision-not-the-bus.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 131. Everything on the mesh speaks to the broker seat, and AMQP is not a provision
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) settled the old broker as an ordinary provider
|
||||||
|
of an ordinary provision, `amqp`, kept for whatever wanted a message broker of its own. The day the
|
||||||
|
bus moved was the day that framing was tested, and it failed in a way that took the control plane
|
||||||
|
down for an evening.
|
||||||
|
|
||||||
|
Three things came out of the wreckage. **The protocol had leaked into the seat's contract**: for a
|
||||||
|
module to hold `mesh-broker`, it had to provide what the seat delivers, and what it delivered was
|
||||||
|
`amqp` — so the module that will carry the bus on NATS could not hold the seat that names the bus,
|
||||||
|
while the module the mesh was leaving could. **A consumer of `amqp` is not asking for AMQP.** The two
|
||||||
|
modules requiring it wanted the mesh's messaging — to emit an event, to hear a topic — and named the
|
||||||
|
wire protocol only because that was the word available. **And AMQP and NATS are not interchangeable
|
||||||
|
at the wire.** A provision named after a protocol can only ever be answered by that protocol, so once
|
||||||
|
the bus is NATS an `amqp` provision has one possible provider, and it is the thing being retired.
|
||||||
|
|
||||||
|
The operator's position, stated during the outage: modules depend on the broker *seat*, not on a
|
||||||
|
protocol; AMQP is obsolete as anything the mesh's core knows about; a module that depends on `amqp`
|
||||||
|
is wrong; and everything should reach the mesh's bus and be able to emit events and consume topics
|
||||||
|
through it.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**A module that needs messaging uses the mesh's bus, and the mesh's bus is whatever holds
|
||||||
|
`mesh-broker`.** Emitting an event and consuming a topic go through the sdk, which is handed the
|
||||||
|
bus by the mesh with the module's own credential. No manifest names a wire protocol to get it.
|
||||||
|
|
||||||
|
**`amqp` is neither a provision nor a requirement.** Registration refuses a manifest that provides
|
||||||
|
it or requires it. The `mesh-broker` seat delivers `mesh-bus`, and its holder is the module that
|
||||||
|
provides `mesh-bus` — today the nats module, and only it.
|
||||||
|
|
||||||
|
**The old broker's module and the two modules that required it leave the catalogue.** They are
|
||||||
|
removed, not converted: one was a proof that a grant worked end to end, the other forwards mail off a
|
||||||
|
queue, and both are re-done against the bus if wanted, as new modules under this record.
|
||||||
|
|
||||||
|
**The controller's AMQP transport is deleted once every node reports on the new bus**, and the
|
||||||
|
switch that selects a transport goes with it — one bus, so nothing to select.
|
||||||
|
|
||||||
|
The predecessor's own broker is outside the mesh and not this record's concern
|
||||||
|
([ADR 0130](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)): what the predecessor's
|
||||||
|
tooling loses when it stops is accepted there.
|
||||||
|
|
||||||
|
## Options considered
|
||||||
|
|
||||||
|
1. **Keep 0127: AMQP stays an ordinary provision with the old broker as its provider.** Rejected. It
|
||||||
|
is what put the protocol into the seat's contract, it is why the seat could be left with no valid
|
||||||
|
holder mid-change, and it keeps two transports in the control plane indefinitely for the benefit of
|
||||||
|
two modules that did not want AMQP in the first place.
|
||||||
|
2. **Bridge it: the old broker's module also provides `mesh-bus`, so both can hold the seat during the
|
||||||
|
change.** Rejected. It makes the retiring broker a legitimate mesh bus for exactly as long as
|
||||||
|
nobody removes the line, which in practice is forever, and it leaves `amqp` as a thing the core
|
||||||
|
still knows the name of.
|
||||||
|
3. **The seat is the dependency; the protocol is nobody's business but the holder's.** Adopted.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **The change of holder is a handover, and it needs a command.** Nothing today moves a seat from
|
||||||
|
one assignment to another as one act, and a seat the control plane dereferences cannot be empty
|
||||||
|
in between — that emptiness is the outage this record comes from. The command takes a seat and the
|
||||||
|
assignment taking it over. Designed and built before the cutover, under
|
||||||
|
[28 — Building the bus](../03-DESIGN/01-to-be/28-building-the-bus.md).
|
||||||
|
- **The seat's row moves to `mesh-bus` before the new holder registers, and that is safe.** The
|
||||||
|
control plane composes its own bus address through the seat *by name*
|
||||||
|
(`${seat:mesh-broker:…}`), and the overview derives holders by name; only registration and the
|
||||||
|
provision-to-seat resolution read what a seat delivers. So the row can change under the current
|
||||||
|
holder without unseating it, the new holder can then register its claim, and the handover happens
|
||||||
|
when both are running. Verified in the code during the outage, not assumed.
|
||||||
|
- **Registration gains two refusals**: a manifest providing `amqp`, and one requiring it.
|
||||||
|
- **The `rollout check` stops saying the old broker stays.** It said so under 0127; it now lists
|
||||||
|
unassigning it as the last step of the move.
|
||||||
|
- **What got harder**: a third party that genuinely wants an AMQP broker on a mesh node runs one as
|
||||||
|
any application module, with no provision and no seat, and nothing on the mesh routes to it. That
|
||||||
|
is the cost of the mesh not knowing the word.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| No manifest provides or requires `amqp` | a registration test refusing each, naming this record; and a whole-catalogue test asserting no registered manifest names it |
|
||||||
|
| `mesh-broker` delivers `mesh-bus`, and only a `mesh-bus` provider may hold it | the existing registration test for a delivering seat, with the row's value read from the store (mesh-controller#89) |
|
||||||
|
| The seat's row can change without unseating the holder | a test composing the control plane's own address and the overview under a row that the current holder does not satisfy |
|
||||||
|
| The rollout does not leave the old broker running | `rollout check` output, asserted in its test |
|
||||||
|
| The AMQP transport is gone | the package does not compile with it referenced; the switch variable is refused as unknown at start |
|
||||||
@@ -135,10 +135,11 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md)
|
- **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md)
|
||||||
- **0119** — [A taken tunnel's predecessor is retired once the take is proven](0119-a-taken-tunnels-predecessor-is-retired.md)
|
- **0119** — [A taken tunnel's predecessor is retired once the take is proven](0119-a-taken-tunnels-predecessor-is-retired.md)
|
||||||
- **0125** — [The bus is the only broker](0125-the-bus-is-the-only-broker.md) *(superseded)*
|
- **0125** — [The bus is the only broker](0125-the-bus-is-the-only-broker.md) *(superseded)*
|
||||||
- **0127** — [AMQP is a provision, not the bus](0127-amqp-is-a-provision-not-the-bus.md)
|
- **0127** — [AMQP is a provision, not the bus](0127-amqp-is-a-provision-not-the-bus.md) *(superseded)*
|
||||||
- **0128** — [The mesh bus is required, not ambient](0128-the-mesh-bus-is-required-not-ambient.md)
|
- **0128** — [The mesh bus is required, not ambient](0128-the-mesh-bus-is-required-not-ambient.md)
|
||||||
- **0129** — [A seat carries the protocol of its role](0129-a-seat-carries-the-protocol-of-its-role.md)
|
- **0129** — [A seat carries the protocol of its role](0129-a-seat-carries-the-protocol-of-its-role.md)
|
||||||
- **0130** — [The predecessor is ending, and its broker goes with it](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)
|
- **0130** — [The predecessor is ending, and its broker goes with it](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)
|
||||||
|
- **0131** — [Everything on the mesh speaks to the broker seat, and AMQP is not a provision](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ code:
|
|||||||
updated: 2026-09-27
|
updated: 2026-09-27
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||||
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
||||||
@@ -303,7 +303,7 @@ the private network. It is raised at genesis like the store, adopted as a module
|
|||||||
phase.
|
phase.
|
||||||
|
|
||||||
**The deprecated broker is an ordinary module, not a compatibility layer.** Revision, second review
|
**The deprecated broker is an ordinary module, not a compatibility layer.** Revision, second review
|
||||||
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)): earlier text here, and
|
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat)): earlier text here, and
|
||||||
ADR 0106 before it, called it `lavinmq-compat` — one purpose, the predecessor's clients, and a
|
ADR 0106 before it, called it `lavinmq-compat` — one purpose, the predecessor's clients, and a
|
||||||
retirement condition of no client connected for a period the operator sets. It is none of those.
|
retirement condition of no client connected for a period the operator sets. It is none of those.
|
||||||
A module may legitimately need an AMQP broker as a **backing service**, the way it needs a
|
A module may legitimately need an AMQP broker as a **backing service**, the way it needs a
|
||||||
@@ -534,7 +534,7 @@ find what changed and why.
|
|||||||
**Still open:**
|
**Still open:**
|
||||||
|
|
||||||
- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
|
- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
|
||||||
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md))): one stream, and not as a
|
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md))): one stream, and not as a
|
||||||
preference — the streams the bus is made of are composed as configuration before any module
|
preference — the streams the bus is made of are composed as configuration before any module
|
||||||
runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping
|
runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping
|
||||||
decides it.
|
decides it.
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ status: implemented
|
|||||||
code:
|
code:
|
||||||
- mesh-controller internal/catalogue/seats.go
|
- mesh-controller internal/catalogue/seats.go
|
||||||
- mesh-controller internal/catalogue/resolve.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/seats.go
|
||||||
- mesh-controller cmd/mesh-controller/source.go
|
- mesh-controller cmd/mesh-controller/source.go
|
||||||
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
||||||
- mesh-catalog modules/gitea/module.json
|
- mesh-catalog modules/gitea/module.json
|
||||||
updated: 2026-09-27
|
updated: 2026-09-27
|
||||||
decisions:
|
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/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.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
|
- 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
|
**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.
|
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
|
**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
|
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
|
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. |
|
| `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. |
|
| 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 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`. |
|
| 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. |
|
| 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. |
|
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ code:
|
|||||||
- mesh-catalog modules/nats
|
- mesh-catalog modules/nats
|
||||||
- mesh-controller internal/catalogue
|
- mesh-controller internal/catalogue
|
||||||
- mesh-lab scenarios
|
- mesh-lab scenarios
|
||||||
updated: 2026-09-27
|
updated: 2026-09-28
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
- 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/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||||
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
||||||
@@ -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
|
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
|
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.
|
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
|
**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
|
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.
|
reaching the new bus. What remains is running it, which is 4.1's bed.
|
||||||
@@ -655,55 +655,72 @@ healthy while reacting to nothing.
|
|||||||
- [ ] 5.1 the cutover bed: a mesh on AMQP with a predecessor stand-in on the deprecated broker
|
- [ ] 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
|
moves its bus in one rollout, every node reporting on NATS afterwards, the stand-in's own
|
||||||
client still connected throughout
|
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.
|
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
|
**Done 2026-09-28, 02:25.** Every machine reports on the new bus, the seat is held by the
|
||||||
there is nothing to inspect afterwards and no half to roll back — either the mesh was ready or it
|
module that provides it, the old broker is unassigned and forgotten, and every credential was
|
||||||
was not. `rollout check` answers that from records, with one dial: is a bus answering, does a
|
minted afresh at the end because two had been printed on the way. What it took, in the order
|
||||||
machine hold the seat, has it been sent the composed user list, does every machine and every
|
it was found, each fixed on the trunk before the next step: the control plane's `serve` and
|
||||||
module that speaks have a credential. Each missing thing names its own next step, because "not
|
`push` never selected the new transport (task 4.3, open until then); a machine's user was
|
||||||
ready" that cannot be acted on is not an answer at the point where the next step is irreversible.
|
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
|
||||||
**A machine with no credential is what must stop it.** It keeps running, cannot come back, and
|
pinning it; the seat table's rows carried no protocol, so no role's work queue was raised; the
|
||||||
afterwards there is no bus to tell it anything over.
|
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
|
||||||
The move itself is deliberately not written yet, and the command says so rather than pretending:
|
is why there is now `rollout hand <node>` and a host adopts a delivered membership at start.
|
||||||
it waits on the check having been run against a real mesh. Writing the irreversible half before
|
The bootstrap loop — a bus that can only be raised by a declaration that can only arrive
|
||||||
the question it depends on has ever been asked of something real is how the plan's own rule about
|
over that bus — was broken once, by hand: the mesh's own composed configuration started the
|
||||||
beds gets broken by another route.
|
server, and the controller binary was run on the node directly until the managed container
|
||||||
|
could be rebuilt over the bus it was on.
|
||||||
> **What this costs if it goes wrong, measured rather than assumed.** On the installation this is
|
|
||||||
> for, the old broker is also what a whole automation layer outside the mesh connects to — so it
|
|
||||||
> stays, as an ordinary provider of `amqp` ([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)),
|
|
||||||
> and this step is not its retirement. 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.
|
|
||||||
- [ ] 5.3 the mesh's own accounts removed from the deprecated broker, and then the broker itself:
|
|
||||||
after the rollout nothing of the mesh speaks to it, and an account nothing uses is one nobody
|
|
||||||
rotates. **It finishes now** ([ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)):
|
|
||||||
the predecessor is deprecated rather than kept, so once its remnants have stopped the module is
|
|
||||||
unassigned and the port is free. No retirement machinery — a provision with no consumers has its
|
|
||||||
provider unassigned, which is ADR 0127 being paid off rather than revised.
|
|
||||||
|
|
||||||
|
- [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.
|
||||||
|
**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)).
|
||||||
|
- [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 (**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)).
|
||||||
Retiring with it: the build outcome's second announcement under the module's own name, which
|
Retiring with it: the build outcome's second announcement under the module's own name, which
|
||||||
exists only so a catalogue deployed before the rename and one deployed after both hear it.
|
existed only so a catalogue deployed before the rename and one after both heard it.
|
||||||
|
|
||||||
> **The remote tooling goes with it too.** The predecessor's own mesh talks over that broker, so
|
> **The remote tooling goes with it too.** The predecessor's own mesh talks over that broker, so
|
||||||
> shutting it down ends the path that reaches this installation's machines from a workstation.
|
> shutting it down ends the path that reaches this installation's machines from a workstation.
|
||||||
> The rollout has to be driven from the node, or driven before the broker stops — which is a
|
> The rollout is driven from the node, or before the broker stops — a sequencing constraint on
|
||||||
> sequencing constraint on 5.2 and not an afterthought.
|
> 5.2, not an afterthought.
|
||||||
|
- [x] 5.5 **the AMQP transport is deleted from the control plane and the hosts**. One bus, nothing
|
||||||
|
to select ([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)).
|
||||||
|
|
||||||
> **5.4 is gone, and was wrong from ADR 0127 onward.** It read "the deprecated broker retires
|
**Done 2026-09-28.** The control plane's old consume loop, build request, tool call, management
|
||||||
> when its condition holds — no client connected for the period the operator sets", which is
|
API and account scoping went, and the host's old dialling and enrolment paths with them; a
|
||||||
> ADR 0106's framing of it as a compatibility module with an end date.
|
membership or token naming any other bus is refused before anything is sent. Nothing selects a
|
||||||
> [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) settled that it is an
|
transport any more: the variable that once did (`MESH_BUS_NATS`) now only names where the
|
||||||
> **ordinary provider** of the `amqp` provision, like any module answering a backing service:
|
control plane reads its own bus credential, the way any module reads a secret. **Checked by the
|
||||||
> no seat, not foundation, and **no retirement condition**, because the day its last client
|
build**: neither repository's module file names the AMQP client library, so a line that still
|
||||||
> disappears is not a day anything is waiting for. Removed rather than reworded — a step that
|
used it would not compile. The store-window guarantee ([issue 083](../../04-ISSUES/083-other-control-messages-are-lost-while-the-store-restarts/00-report.md))
|
||||||
> waits for a condition nobody set would sit open forever.
|
is tested against a bus-less fake rather than the old transport's memory, which is what let
|
||||||
|
that memory go — the one thing it did that the stream does not (superseding a held report) is
|
||||||
|
the staleness check on the message itself (design 25 §3).
|
||||||
|
|
||||||
|
Found on the way: **no build had ever recorded what it stood on.** A recipe reads its base from
|
||||||
|
a build argument, so the digest was never in the file the builder derived edges from, and every
|
||||||
|
order that says *bases first* — `build --on`, `build --behind`, the merge follow-up of
|
||||||
|
[issue 131](../../04-ISSUES/131-nothing-tells-the-mesh-a-source-moved/00-report.md) — walked a
|
||||||
|
graph with no edges. The builder now reports the bases it was handed, the control plane records
|
||||||
|
them by artifact path, and the graph is read from the newest build of each module — a recorded
|
||||||
|
manifest carries no `build.on`, so the edge is derived from the build or it does not exist.
|
||||||
|
|
||||||
|
> **The old 5.4 note is history.** It recorded that a retirement *condition* was wrong from
|
||||||
|
> [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) onward, which framed the old broker
|
||||||
|
> as an ordinary provider with no end. [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) ends that
|
||||||
|
> framing in turn: the broker is not kept as a provider either, because AMQP is not a provision. Both
|
||||||
|
> readings are kept here so the two reversals can be read in order.
|
||||||
|
|
||||||
**Done when.** Every node reports on NATS, and nothing of the mesh's own is left connected to the
|
**Done when.** Every node reports on NATS, and nothing of the mesh's own is left connected to the
|
||||||
deprecated broker.
|
deprecated broker.
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ controller — because the bus's accounts are configuration rather than somethin
|
|||||||
creates, so there is no provisioner process in the path and nothing waiting on a bus account in
|
creates, so there is no provisioner process in the path and nothing waiting on a bus account in
|
||||||
order to make bus accounts. A module that runs a NATS server of its own and offers it as a
|
order to make bus accounts. A module that runs a NATS server of its own and offers it as a
|
||||||
backing service provides **`nats`**, exactly as the deprecated broker provides `amqp`
|
backing service provides **`nats`**, exactly as the deprecated broker provides `amqp`
|
||||||
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)).
|
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat)).
|
||||||
|
|
||||||
They are never the same name. A manifest saying `nats` could otherwise mean either the mesh's
|
They are never the same name. A manifest saying `nats` could otherwise mean either the mesh's
|
||||||
nervous system or a private queue, and the difference between those is the whole architecture.
|
nervous system or a private queue, and the difference between those is the whole architecture.
|
||||||
@@ -427,7 +427,7 @@ it as an ordinary module once the registry exists.
|
|||||||
|
|
||||||
So there are exactly two things the normal path cannot make, both at genesis, both ending the
|
So there are exactly two things the normal path cannot make, both at genesis, both ending the
|
||||||
moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in
|
moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in
|
||||||
[ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)) — a provisioner is a module and
|
[ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)) — a provisioner is a module and
|
||||||
needs an account before it can run) and **the vault's own credential**. Any third exception is a
|
needs an account before it can run) and **the vault's own credential**. Any third exception is a
|
||||||
design failure, and naming these two is what makes a third one visible.
|
design failure, and naming these two is what makes a third one visible.
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
|
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
|
||||||
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **Proposed.** The mesh models machines but not the humans on them: a node gains operator accounts, and a resource may live under a home owned by its account — what would own ~/.ssh, dotfiles and ~/.config when HAL retires | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
|
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **Proposed.** The mesh models machines but not the humans on them: a node gains operator accounts, and a resource may live under a home owned by its account — what would own ~/.ssh, dotfiles and ~/.config when HAL retires | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
|
||||||
|
|
||||||
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-27
|
||||||
|
located-in: [mesh-catalog modules/gitea, mesh-controller cmd/mesh-controller, mesh-controller internal/builder]
|
||||||
|
fixed-by: mesh-catalog #124 — the forge module watches for merged pull requests and emits `pull.merged` with the merge commit and the clone address; mesh-controller #110/#111 — the control plane follows that event on the bus, marks every module built from that repository and branch as moved, and builds them bases first, stopping when a base fails; mesh-controller #113/#114 — a build records the bases it was handed and the graph is read from builds, without which "bases first" had no edges to order by.
|
||||||
|
amended-design: 03-DESIGN/01-to-be/28-building-the-bus.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 131 — Nothing tells the mesh a source moved, and it reports itself current anyway
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Six changes were merged to the trunk of six repositories in one sitting. The build machine built
|
||||||
|
nothing. Its last build, minutes before the first merge, was still the one it reported; no build was
|
||||||
|
requested, refused or failed, because none was ever asked for.
|
||||||
|
|
||||||
|
Asked afterwards what was wrong, the mesh said:
|
||||||
|
|
||||||
|
> 4 machine(s), all doing what they were told, all heard from, running what the mesh would send them,
|
||||||
|
> and every module current with its source
|
||||||
|
|
||||||
|
Every one of the six had moved. The last clause was false for all of them, and it is the clause a
|
||||||
|
person reads to decide whether there is anything to do.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
**The mesh learns a source moved by being told, and there is no longer anything to tell it.** The
|
||||||
|
command exists — a person names the module and the commit — and so does the question the overview
|
||||||
|
answers. What is missing is whatever used to connect the two. One repository still carries a forge
|
||||||
|
webhook aimed at a port; the rest carry none, and the port belongs to a different service than the
|
||||||
|
one the arrangement implies. So the state is not "the trigger is broken" but "there is no trigger,
|
||||||
|
and nothing says so".
|
||||||
|
|
||||||
|
**A wrong answer is worse here than no answer.** "Every module current with its source" is
|
||||||
|
indistinguishable, to a reader, from a mesh that has genuinely caught up. The overview is built to be
|
||||||
|
the thing you check instead of checking by hand, so a confident false negative removes the habit that
|
||||||
|
would otherwise have caught it. Nothing in the mesh is at fault for being out of date — it is at
|
||||||
|
fault for saying it is not.
|
||||||
|
|
||||||
|
**It is also why "current with its source" cannot be a stored fact.** The mesh compares what it built
|
||||||
|
against what it was last told the source was, and calls that agreement. Two facts agreeing tells you
|
||||||
|
nothing when both come from the same place.
|
||||||
|
|
||||||
|
## The intended shape, which is decided and not built
|
||||||
|
|
||||||
|
The forge emits what happened to it — a pull request merged — and the build machine reacts by
|
||||||
|
building what that commit affects. That keeps the forge ignorant of the build system and the build
|
||||||
|
machine ignorant of the forge's internals, which is the same argument
|
||||||
|
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) makes for addressing an event
|
||||||
|
to its emitter: the merge is a fact about the forge, and what should be rebuilt because of it is not
|
||||||
|
the forge's business to know.
|
||||||
|
|
||||||
|
The forge's module already declares the event. The build machine declares that it consumes nothing.
|
||||||
|
|
||||||
|
## What the trigger cannot be
|
||||||
|
|
||||||
|
**Not one build per changed module.** The modules form a graph: several are built from one
|
||||||
|
repository, and some are the base another is compiled on — a runtime image, a compiler base, a
|
||||||
|
repository whose context a second module builds from. Firing a build for each changed module
|
||||||
|
independently would start work that cannot succeed yet and produce a failure per dependent, for one
|
||||||
|
cause.
|
||||||
|
|
||||||
|
Observed while catching the mesh up by hand on 2026-09-27: a compiler base had to move before
|
||||||
|
anything compiled against it could build, and when it failed, the right behaviour was for its
|
||||||
|
dependents to wait rather than each fail the same way. Fifteen modules shared the cause. A trigger
|
||||||
|
that reports it fifteen times has buried it.
|
||||||
|
|
||||||
|
So whatever reacts to the forge's event resolves what changed into an order, builds the bases first,
|
||||||
|
and holds a dependent while its base is unbuilt or failed. That is a larger thing than "rebuild what
|
||||||
|
the commit touched", and knowing it now is cheaper than discovering it from fifteen identical
|
||||||
|
failures.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Is "the source moved" still a thing a person can assert by hand once the event path exists, or does
|
||||||
|
the hand-operated form become the thing that made this failure possible?
|
||||||
|
- Which commit does the build machine act on — the merge, or each commit it brought — and what does
|
||||||
|
it do when several arrive for one module at once?
|
||||||
|
- How does the overview stop being able to lie? Comparing what was built against what was recorded
|
||||||
|
will always agree. Whether the trunk has moved is a question only the forge can answer, so either
|
||||||
|
the overview asks it, or it stops claiming to know.
|
||||||
|
- Does this want to be the same mechanism as the build request on the bus
|
||||||
|
([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)), or does it sit in
|
||||||
|
front of it?
|
||||||
|
|
||||||
|
## What was done (2026-09-28)
|
||||||
|
|
||||||
|
The shape above was built as described: the forge's module emits the merge, the control plane
|
||||||
|
consumes it, and nothing on either side knows the other's internals. The build is asked for the
|
||||||
|
merge commit, not each commit the merge brought — the trunk moved once, to one place. Several merges
|
||||||
|
for one module arriving in a row are followed in turn, each moving the recorded source to its own
|
||||||
|
commit, so the last one to arrive is the one the mesh ends up built from.
|
||||||
|
|
||||||
|
The hand-operated form stays. `module moved` is how a source is recorded without a forge — a module
|
||||||
|
built from a repository elsewhere, or a mesh whose forge module is down — and it is the same act the
|
||||||
|
event performs, so the two cannot disagree about what "moved" means.
|
||||||
|
|
||||||
|
**Bases first needed edges, and there were none.** The order this report asked for was written and
|
||||||
|
walked a graph that no build had ever recorded: a recipe reads its base from a build argument, so the
|
||||||
|
digest was never in the file the builder read edges from. A build now reports what it was handed, the
|
||||||
|
control plane records it by artifact path, and the order is read from each module's newest build.
|
||||||
|
|
||||||
|
**What still can lie.** The overview compares what was built against where it was last told the
|
||||||
|
source is; the forge's event is now what moves that mark, so it is right for as long as the forge
|
||||||
|
module was listening. A merge made while that module was down is a merge the mesh does not know of
|
||||||
|
until the module next polls — it announces what merged since it last looked, so the gap closes when
|
||||||
|
it comes back, and not before. The overview does not say so.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-28
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller]
|
||||||
|
fixed-by: mesh-controller — `module add` takes `--path` and `--self`, so a module handed over by hand records the whole location it came from; a record naming a repository and no directory says so in the reply; the rule is one function with a test beside it. The nine records already wrong were corrected by rebuilding each with its real directory, which is the same act through the same door.
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 132 — A module can be recorded without the directory it lives in
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Nine modules on one mesh could not be rebuilt. Each attempt failed the same way:
|
||||||
|
|
||||||
|
> has no module.json at its root, so there is nothing saying what it is
|
||||||
|
|
||||||
|
All nine were recorded as coming from a repository that holds many modules, each in its own
|
||||||
|
directory — and each record named the repository and no directory. So every build cloned the
|
||||||
|
repository and looked for a manifest where there has never been one.
|
||||||
|
|
||||||
|
The failure only surfaced when something asked for all of them at once. Before that, the overview
|
||||||
|
said every module was current with its source, because what it compares is what was built against
|
||||||
|
what the mesh was last told the source has, and neither half knows whether the source can be found
|
||||||
|
at all.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
**A module is a repository and a directory inside it** ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)),
|
||||||
|
and one of the two doors into the catalogue could record only the first half. A build records the
|
||||||
|
directory it was given, so a module that arrived by being built is always whole; a module handed over
|
||||||
|
by hand had no way to say where it lived, and the flag to say it did not exist. The rule was decided
|
||||||
|
and enforced on one path out of two.
|
||||||
|
|
||||||
|
**Half a location reads exactly like a whole one.** Nothing in the record is empty in a way a person
|
||||||
|
would notice: the repository is there, the branch is there, the commit is there. The mesh only finds
|
||||||
|
out at the moment it needs the manifest, which is the moment it is trying to rebuild — and the module
|
||||||
|
stays on whatever it last built, indefinitely, with nothing saying why.
|
||||||
|
|
||||||
|
**It is the same shape as [131](../131-nothing-tells-the-mesh-a-source-moved/00-report.md).** A
|
||||||
|
comparison between two facts the mesh holds about itself will agree with itself. Whether the source
|
||||||
|
can be found is a question only an attempt to read it answers, and the answer had nowhere to go.
|
||||||
|
|
||||||
|
## What was done
|
||||||
|
|
||||||
|
`module add` takes the directory and which forge holds the repository, so a hand-registered module
|
||||||
|
records the same whole location a built one does. What a record must say to be worth anything is one
|
||||||
|
function with a test beside it, rather than a paragraph in a help string: provenance together or not
|
||||||
|
at all, a directory needs a repository to be inside, a path on the mesh's own forge is not an address.
|
||||||
|
And a record that names a repository but no directory says so when it is made — not refused, because a
|
||||||
|
module really at a repository's root is ordinary, but said, because the person adding it is the one
|
||||||
|
who knows which it is.
|
||||||
|
|
||||||
|
The nine wrong records were corrected by building each with its real directory, which re-records it.
|
||||||
|
No row was written by hand.
|
||||||
|
|
||||||
|
## What is still true
|
||||||
|
|
||||||
|
A directory that does not exist in the repository cannot be refused when the module is added: the
|
||||||
|
control plane does not clone, and inventing a check there would mean it did. The first build says so
|
||||||
|
plainly, which is one build rather than nine, and the record it leaves behind is right from then on.
|
||||||
Reference in New Issue
Block a user