From 814c9e563f2ef15ec30ec5c7dcfb57d1bcf59591 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 19:26:07 +0200 Subject: [PATCH] The bus is the only broker; step 1 starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A module does declare requirements the provisioner fulfils — but the broker it gets that way is a private vhost, the analog of a database, not the mesh's bus. Two modules of the new mesh depend on it, so the compatibility broker was never single-purpose and its retirement would have stranded them. NATS is the heart: one bus, a module's messaging is subjects on it scoped by what it declares, and no module is handed a server of its own. The seat delivers nothing; the interface retires with the broker. Also closes the EVENTS question — one stream, on the bootstrap argument, not preference. Designs 25 and 28 go in-progress: step 1 is starting. The insight check caught a false positive on its own first real use — its bold-run pattern crossed newlines and joined an unrelated `**` to the marker. Constrained to one line, still catching all four bad shapes. --- 00-META/checks/records.py | 7 +- 02-DECISIONS/0106-the-bus-is-nats.md | 13 ++ .../0117-the-bus-is-the-only-broker.md | 132 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/25-the-bus-on-nats.md | 25 +++- 03-DESIGN/01-to-be/28-building-the-bus.md | 8 +- 6 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 02-DECISIONS/0117-the-bus-is-the-only-broker.md diff --git a/00-META/checks/records.py b/00-META/checks/records.py index 0484dd8..ed6c186 100644 --- a/00-META/checks/records.py +++ b/00-META/checks/records.py @@ -299,8 +299,11 @@ def check_progressive_insights(failures, records): unmarked change stands out as the anomaly it is. """ phrase = re.compile(r"progressive insight", re.I) - marker = re.compile(r"\*\*Progressive insights?\s*[\u2014\u2013-]\s*(\d{4}-\d{2}-\d{2})\.?\*\*") - loose = re.compile(r"\*\*[^*]*[Pp]rogressive insights?[^*]*\*\*") + # Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across + # newlines will happily join an unrelated `**` far above to the marker below, reporting the + # whole span between them. It did exactly that the first time this ran. + marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})\.?\*\*") + loose = re.compile(r"\*\*[^*\n]*[Pp]rogressive insights?[^*\n]*\*\*") iso = re.compile(r"^\d{4}-\d{2}-\d{2}$") for number, record in sorted(records.items()): diff --git a/02-DECISIONS/0106-the-bus-is-nats.md b/02-DECISIONS/0106-the-bus-is-nats.md index 6da6c77..a12110d 100644 --- a/02-DECISIONS/0106-the-bus-is-nats.md +++ b/02-DECISIONS/0106-the-bus-is-nats.md @@ -75,6 +75,19 @@ after its deliveries are exhausted; a module's account cannot publish outside it subscribe outside its `consumes`. Then the cutover bed: a mesh on AMQP with the predecessor's compatibility broker beside it moves its bus in one rollout with every node reporting afterwards. +## Progressive insight + +> **Progressive insight — 2026-09-26.** *The compatibility broker was not single-purpose when this +> was written.* This record says the adopted AMQP broker is "kept as a module with one purpose — +> the predecessor's clients". Two modules of the new mesh also depended on it, through a `requires: +> ["amqp"]` grant its provisioner answered with a private vhost — `amqp-ping` and +> `amqp-email-forwarder`. On the retirement condition below, both would have been left requiring +> something no provider answers. +> [ADR 0117](0117-the-bus-is-the-only-broker.md) resolves it by moving them onto the bus and +> retiring the interface, which makes this record's sentence true rather than merely intended. The +> decision — the bus is NATS, the AMQP broker becomes the predecessor's compatibility broker and +> retires with the last of them — is unchanged. + ## References - [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md) diff --git a/02-DECISIONS/0117-the-bus-is-the-only-broker.md b/02-DECISIONS/0117-the-bus-is-the-only-broker.md new file mode 100644 index 0000000..65be793 --- /dev/null +++ b/02-DECISIONS/0117-the-bus-is-the-only-broker.md @@ -0,0 +1,132 @@ +--- +topic: the mesh +status: accepted +date: 2026-09-26 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0106-the-bus-is-nats.md +--- + +# 117. The bus is the only broker + +## Context + +[ADR 0106](0106-the-bus-is-nats.md) moved the mesh's bus to NATS and kept the AMQP broker "as a +module with one purpose — the predecessor's clients", retiring with the last of them. +[Design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) repeats that: a compatibility module with +a single purpose and a retirement condition. + +**It is not single-purpose, and was not when that was written.** Two modules of the *new* mesh +declare `requires: ["amqp"]` and are answered by the broker module's own provisioner: + +- `amqp-ping`, whose source says it "exists to PROVE the grant end to end: the mesh gave it a + scoped login and a vhost of that name on the lavinmq provider"; +- `amqp-email-forwarder`, which uses it for work. + +What that provisioner answers is **not the mesh's bus**. Its own comment draws the line: a +consumer gets "its own message broker, isolated from every other consumer's by the vhost +boundary… a broker of its own, not a shared account on the mesh's control-plane broker" — +vhost-per-login, "the exact analog of postgres's database-per-login." + +So two different things wear the word *broker*: the mesh's nervous system, and a private message +broker handed to a module as a resource, the way a database is. The first is being replaced. The +second was never examined, and on the retirement condition ADR 0106 sets, it disappears with no +successor and nothing notices — a module of the new mesh left requiring something no provider +answers. + +The operator's direction, asked at the point this surfaced: **NATS is the heart of the +application** — not a component it contains, and not a thing to reproduce the predecessor's +shapes on. + +## Considered Options + +1. **Carry the private broker forward onto NATS** — each requiring module gets its own NATS + account, provisioned like a database. Rejected on three counts. It reproduces the + predecessor's shape on the new bus, which is the thing this whole move exists to stop. It + gives the mesh two messaging models, so "how does a module send a message" has two answers + depending on a manifest line. And NATS accounts isolate subject spaces *entirely*: a module + inside its own account cannot reach the mesh's bus at all, so it would hold two connections + and two identities to do one job. +2. **Keep the compatibility broker indefinitely** for the mesh's own modules. Rejected: its + retirement condition is the point of it. A module of the new mesh depending on the retired one + keeps the predecessor alive permanently, which is the opposite of a compatibility module. +3. **One bus. A module's messaging is subjects on it, scoped by what it declares.** Adopted. + +## Decision + +**The bus is the only broker.** NATS is the mesh's one messaging system, and every module's +messaging is subjects on that bus under its own account, scoped by its `emits` and `consumes` +([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)). There is no second +broker, and none is handed to a module as a resource. + +**The `amqp` interface is not carried forward.** It leaves the set of things a module may require +and retires with the compatibility broker rather than gaining a successor. + +Concretely, in the controller's seat table: **the `mesh-broker` seat delivers nothing.** It +currently reads `Delivers: "amqp"` — the seat's holder answers a requirement for a broker — and +under this decision it joins `mesh-controller` and `the-catalogue`, the foundation seats that +deliver no provision at all. The bus is not something a module asks for; it is what a module is +reached through. + +- `amqp-email-forwarder` moves to the bus like any module: what it emits and consumes, declared, + and the account follows. +- `amqp-ping`'s *purpose* is kept and its mechanism is not. Proving end to end that a module + receives scoped messaging it did not configure itself is worth a probe; it becomes a probe of + the bus, and its assertion changes from "I reached my own vhost" to "I reached exactly my + subjects and was refused the rest." + +**A module that wants a queue of its own has one already**: a subject nothing else may publish to +and a durable consumer of its own, both derived from its declaration. What it does not get is a +server of its own. + +**The mesh's own streams are the controller's, created at genesis, not provisioned** — and +`EVENTS` is one stream, closing the question [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) +§11 left open. The reason is not preference but **bootstrapping**: a provisioner is a module, and +a module needs a bus account before it can run at all. Anything the bus itself is made of must +exist before the first module starts, so it is composed as configuration +([ADR 0106](0106-the-bus-is-nats.md): never through a management API) rather than provisioned by +something that could not yet be running. + +## Consequences + +- **Design 25 gains the distinction and loses the "single purpose" claim**; its §11 question about + the `EVENTS` stream closes here. +- **Nothing in [design 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)'s + model changes** — the four provider kinds, the contract, resolution all stand, and it never + enumerated interfaces, so there is nothing to strike from it. What changes is that messaging + leaves the set of things resolved at all: every module has it by existing. +- **One line of the controller's seat table changes**, and it is the load-bearing one: + `mesh-broker` stops declaring what it delivers. A requirement for `amqp` then resolves to + nothing and is refused at assignment, which is how the two modules below are found rather than + discovered at runtime. +- **Two modules have conversion work**, and it belongs to step 4 of + [ADR 0116](0116-the-bus-is-built-in-five-steps.md), with the flows. Neither blocks step 1. +- **The compatibility broker becomes what ADR 0106 already called it** — single-purpose — once + those two have moved. That record's claim was wrong when written and is made true by this one. +- **What got harder:** a module that genuinely wanted an isolated server — a tenant boundary at + the broker rather than at the subject — no longer has that option, and would have to argue for + it as a new decision. That is the intended cost: one bus is the point. + +## How it is checked + +- **A module's messaging works with no `requires` line for it.** A lab bed: a module declaring + only `emits` and `consumes` reaches its subjects, and is refused every other — which is + [ADR 0116](0116-the-bus-is-built-in-five-steps.md) step 1's permission bed, already required. +- **Nothing requires `amqp`.** With the seat delivering nothing, a module still declaring it is + refused at resolution — the existing "requirement no provider answers" path, not a new check. A + catalogue test asserts no module declares it once the two have moved. +- **The probe proves the claim it is named for.** `amqp-ping`'s successor fails if a module can + reach a subject outside its declaration, not merely if it cannot reach its own. +- **The compatibility broker's retirement condition can actually be met.** A check that no module + of the mesh — as opposed to a predecessor client — holds a connection to it. + +## References + +- [ADR 0106](0106-the-bus-is-nats.md) — the bus is NATS; corrected here on what the compatibility + broker serves. +- [ADR 0116](0116-the-bus-is-built-in-five-steps.md) — the steps; the conversions land in step 4. +- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — the scoping that + makes one bus safe. +- [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md), + [design 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — the two documents + this changes. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 67eb0fe..8e1658b 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -133,6 +133,7 @@ python3 00-META/checks/index.py fail if stale - **0105** — [The mesh adopts the predecessor's tunnel in place](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md) - **0106** — [The bus is NATS](0106-the-bus-is-nats.md) - **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md) +- **0117** — [The bus is the only broker](0117-the-bus-is-the-only-broker.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/25-the-bus-on-nats.md b/03-DESIGN/01-to-be/25-the-bus-on-nats.md index 6b92b3a..280c733 100644 --- a/03-DESIGN/01-to-be/25-the-bus-on-nats.md +++ b/03-DESIGN/01-to-be/25-the-bus-on-nats.md @@ -1,6 +1,6 @@ --- layer: to-be -status: proposed +status: in-progress code: - mesh-controller internal/link (to be replaced) - mesh-host internal/link (to be replaced) @@ -10,6 +10,7 @@ code: updated: 2026-09-26 decisions: - 02-DECISIONS/0106-the-bus-is-nats.md + - 02-DECISIONS/0117-the-bus-is-the-only-broker.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/0079-the-foundation-seats-are-named-after-their-servers.md @@ -188,8 +189,19 @@ current. Nothing is declared as `reload-on` or `restart-on` for this resource at Its guard is the same rule as the AMQP broker's: the monitoring port is refused from anything but the private network. It is raised at genesis like the store, adopted as a module in the same -phase. The predecessor's AMQP broker remains a module of its own, `lavinmq-compat`, with a single -purpose and a retirement condition: no client connected for a period the operator sets. +phase. The predecessor's AMQP broker remains a module of its own, `lavinmq-compat`, with a +retirement condition: no client connected for a period the operator sets. + +**And with one purpose, which it did not have when this was written.** Revision, second review +([ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)): two modules of the new mesh +required a broker *of their own* from it — a private vhost per consumer, the analog of +database-per-login, which is a different thing from the mesh's bus and was never examined here. +**There is one bus, and no module is handed a broker as a resource.** A module's messaging is +subjects on the bus under its own account, scoped by its `emits` and `consumes`; a module that +wants a queue of its own has a subject nothing else may publish to and a durable consumer, both +from its declaration. What it does not get is a server of its own. The `amqp` interface retires +with the compatibility broker instead of gaining a successor, and the two modules move to the bus +in step 4. ## 6. Joining: the enrolment handshake @@ -398,8 +410,11 @@ find what changed and why. **Still open:** -- Whether EVENTS should be one stream or one per emitting module (retention per module vs. one - policy). One stream is proposed; the review may disagree. +- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed** + ([ADR 0117](../../02-DECISIONS/0117-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 + runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping + decides it. - The heartbeat interval and the controller's "quiet" threshold on core NATS without persistence — the same numbers as today are proposed. - Whether the person's client is a catalogue module (runs on an enrolled workstation node) or a diff --git a/03-DESIGN/01-to-be/28-building-the-bus.md b/03-DESIGN/01-to-be/28-building-the-bus.md index b0352f9..96501c5 100644 --- a/03-DESIGN/01-to-be/28-building-the-bus.md +++ b/03-DESIGN/01-to-be/28-building-the-bus.md @@ -1,11 +1,15 @@ --- layer: to-be -status: proposed -code: [] +status: in-progress +code: + - mesh-catalog modules/nats + - mesh-controller internal/catalogue + - mesh-lab scenarios updated: 2026-09-26 decisions: - 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md - 02-DECISIONS/0106-the-bus-is-nats.md + - 02-DECISIONS/0117-the-bus-is-the-only-broker.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/0100-a-node-in-use-is-adopted-before-it-is-converged.md