The bus is the only broker; step 1 starts
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.
This commit is contained in:
@@ -299,8 +299,11 @@ def check_progressive_insights(failures, records):
|
|||||||
unmarked change stands out as the anomaly it is.
|
unmarked change stands out as the anomaly it is.
|
||||||
"""
|
"""
|
||||||
phrase = re.compile(r"progressive insight", re.I)
|
phrase = re.compile(r"progressive insight", re.I)
|
||||||
marker = re.compile(r"\*\*Progressive insights?\s*[\u2014\u2013-]\s*(\d{4}-\d{2}-\d{2})\.?\*\*")
|
# Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across
|
||||||
loose = re.compile(r"\*\*[^*]*[Pp]rogressive insights?[^*]*\*\*")
|
# 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}$")
|
iso = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
|
|
||||||
for number, record in sorted(records.items()):
|
for number, record in sorted(records.items()):
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
## References
|
||||||
|
|
||||||
- [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md)
|
- [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.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.
|
||||||
@@ -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)
|
- **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)
|
- **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)
|
- **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
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: in-progress
|
||||||
code:
|
code:
|
||||||
- mesh-controller internal/link (to be replaced)
|
- mesh-controller internal/link (to be replaced)
|
||||||
- mesh-host internal/link (to be replaced)
|
- mesh-host internal/link (to be replaced)
|
||||||
@@ -10,6 +10,7 @@ code:
|
|||||||
updated: 2026-09-26
|
updated: 2026-09-26
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 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/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
|
||||||
@@ -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
|
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
|
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
|
phase. The predecessor's AMQP broker remains a module of its own, `lavinmq-compat`, with a
|
||||||
purpose and a retirement condition: no client connected for a period the operator sets.
|
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
|
## 6. Joining: the enrolment handshake
|
||||||
|
|
||||||
@@ -398,8 +410,11 @@ find what changed and why.
|
|||||||
|
|
||||||
**Still open:**
|
**Still open:**
|
||||||
|
|
||||||
- Whether EVENTS should be one stream or one per emitting module (retention per module vs. one
|
- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
|
||||||
policy). One stream is proposed; the review may disagree.
|
([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 heartbeat interval and the controller's "quiet" threshold on core NATS without persistence —
|
||||||
the same numbers as today are proposed.
|
the same numbers as today are proposed.
|
||||||
- Whether the person's client is a catalogue module (runs on an enrolled workstation node) or a
|
- Whether the person's client is a catalogue module (runs on an enrolled workstation node) or a
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: in-progress
|
||||||
code: []
|
code:
|
||||||
|
- mesh-catalog modules/nats
|
||||||
|
- mesh-controller internal/catalogue
|
||||||
|
- mesh-lab scenarios
|
||||||
updated: 2026-09-26
|
updated: 2026-09-26
|
||||||
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/0117-the-bus-is-the-only-broker.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
|
||||||
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
|
|||||||
Reference in New Issue
Block a user