Research 014: the bus on NATS — decide now, build in the lab, cut over once after the core
Measured: AMQP is spoken in three places of the mesh's own code and in none of the sdk or the modules; the predecessor's world is AMQP and retiring. NATS answers every guarantee the bus relies on, durability via JetStream. Recommended: not underneath the migration, not after it either — in parallel, one rehearsed rollout.
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-09-23
|
||||||
|
touches:
|
||||||
|
- 02-DECISIONS/0002-nodes-communicate-over-a-broker.md
|
||||||
|
- 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
|
||||||
|
- 02-DECISIONS/0039-the-broker-client-lives-in-the-runtime-not-the-sdk.md
|
||||||
|
- 02-DECISIONS/0041-events-are-a-relationship.md
|
||||||
|
- 02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md
|
||||||
|
- 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
||||||
|
- 02-DECISIONS/0078-the-store-and-broker-are-modules.md
|
||||||
|
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 014 — The bus on NATS: replace the broker, and when
|
||||||
|
|
||||||
|
**The question, asked mid-migration:** the operator wants the mesh's bus — today an AMQP broker —
|
||||||
|
replaced by NATS, with everything the broker does today. Do we finish the migration on AMQP and
|
||||||
|
move to NATS after, or go to NATS directly?
|
||||||
|
|
||||||
|
**The short answer: decide NATS now, build it in the lab in parallel, and cut the mesh's bus over
|
||||||
|
in one rehearsed rollout — after the migration's core is done and never underneath it.** "First
|
||||||
|
everything on AMQP" is already the state and costs nothing more: the predecessor's broker was
|
||||||
|
merged into the mesh's tonight, and every module converted from here on targets the sdk's broker
|
||||||
|
contract, which names no protocol. The only code that speaks AMQP is the mesh's own, in three
|
||||||
|
places, and it is swapped once.
|
||||||
|
|
||||||
|
## What was measured
|
||||||
|
|
||||||
|
**Where AMQP is spoken** (non-test files): the controller's `internal/link` (5 files, ~2.4k lines
|
||||||
|
with tests) and the builder's main; the host's `internal/link` (~1.5k); the tool runtime's
|
||||||
|
`broker-amqp.ts` (~600 with its main). **The sdk speaks none** — its `Broker` is `request`,
|
||||||
|
`handle`, `publish`, `subscribe`, `close` ([ADR 0039](../../02-DECISIONS/0039-the-broker-client-lives-in-the-runtime-not-the-sdk.md)
|
||||||
|
put the client in the runtime for exactly this). **Every module's tools and events go through the
|
||||||
|
sdk**; one module (`amqp-ping`, a probe) talks AMQP on purpose.
|
||||||
|
|
||||||
|
**What the bus carries:** a control queue (`control`, `.upgrades`, `.catchup`), one queue per node
|
||||||
|
(`node.<name>`), `builds`, the enrolment/report/alive/built flows, a topic exchange of events
|
||||||
|
(`mesh.events.<module>.<event>`, dead-lettered to `mesh.events.dead`), an RPC exchange (`mesh.rpc`)
|
||||||
|
and per-tool service queues (`serve.<module>.<tool>`), plus an MQTT exchange.
|
||||||
|
|
||||||
|
**What it relies on:** TLS for the bus a node enrols over; prefetch with reject/nack so the control
|
||||||
|
queue **holds messages unacknowledged while the store restarts** and retries
|
||||||
|
([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)); publisher confirms and
|
||||||
|
mandatory routing; a dead-letter exchange; **per-module accounts scoped by `emits`/`consumes`**
|
||||||
|
([ADR 0043](../../02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)),
|
||||||
|
minted through the broker's management API; the broker as a module holding the `mesh-broker` seat
|
||||||
|
([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)).
|
||||||
|
|
||||||
|
**The predecessor's world**, now on the same broker: ~50 AMQP connections per machine from four
|
||||||
|
machines, 595 queues, four vhosts, six users. All of it AMQP, all of it retiring module by module.
|
||||||
|
NATS speaks no AMQP: that world cannot move; it does not need to.
|
||||||
|
|
||||||
|
## How NATS answers each of those
|
||||||
|
|
||||||
|
| The mesh relies on | NATS | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| topic routing keys | subjects with wildcards | same shape (`mesh.events.>`) |
|
||||||
|
| RPC and tool invocation over an exchange + reply queue | request/reply, native | simpler than today |
|
||||||
|
| competing consumers | queue groups | same |
|
||||||
|
| durable control queue, hold-unacked-and-retry, catch-up | JetStream: streams, durable consumers, ack/nak with delay, replay | the 0083 guarantee moves to JetStream; core NATS alone is at-most-once and would not do |
|
||||||
|
| dead-letter | max-deliver + advisories, or a stream fed from them | different mechanism, same effect |
|
||||||
|
| TLS bus | TLS | same |
|
||||||
|
| accounts scoped by emits/consumes, vhosts | accounts (isolation) with users and per-subject publish/subscribe permissions | stronger than today; the controller writes an auth config the host declares and the server reloads, instead of calling a management API |
|
||||||
|
| management API | `nats` CLI and an HTTP monitoring endpoint | no vhost concept — accounts instead |
|
||||||
|
| MQTT | built in | same |
|
||||||
|
| a module seat `mesh-broker` | unchanged — the seat is the server, the module changes | ADR 0079 |
|
||||||
|
| multi-node | clusters and leaf nodes | not needed now; a leaf per node is a later question |
|
||||||
|
|
||||||
|
Nothing the mesh needs is missing. The differences are in the shape of durability (JetStream must
|
||||||
|
be declared, streams and consumers are objects) and of accounts (configuration, not API calls).
|
||||||
|
|
||||||
|
## The cost, honestly
|
||||||
|
|
||||||
|
The bus is the mesh's nervous system. Moving it means: a new `nats` module in the catalogue taking
|
||||||
|
the `mesh-broker` seat; the controller's and the host's link packages rewritten; the tool runtime's
|
||||||
|
client swapped behind the unchanged contract; enrolment, reports, builds and the guard's ports
|
||||||
|
re-derived; the lab beds that prove the bus (store window, enrolment, upgrades) re-run on the new
|
||||||
|
one; and eight decisions amended or superseded. Weeks, not days — and none of it can be done
|
||||||
|
halfway on a live mesh: the controller, every host and every tool runtime move together.
|
||||||
|
|
||||||
|
## The sequencing question, answered
|
||||||
|
|
||||||
|
1. **NATS first, directly.** Stalls the migration for the length of the build; the mesh's bus
|
||||||
|
changes under a half-migrated node; the predecessor's clients still need AMQP, so a second
|
||||||
|
broker runs anyway, plus a bridge for whatever crosses. Rejected.
|
||||||
|
2. **Finish on AMQP, NATS after.** Wastes nothing — no module written from here on speaks AMQP —
|
||||||
|
but leaves the decision unmade while modules are written, and the runtimes idle. Adequate.
|
||||||
|
3. **Decide NATS now; build it in the lab in parallel; cut the mesh's bus over in one rehearsed
|
||||||
|
rollout after the core is migrated.** The predecessor's clients never notice: their broker is
|
||||||
|
the one the mesh adopted, kept as a compatibility module with an end date — the day the last
|
||||||
|
AMQP client is gone. **Recommended.**
|
||||||
|
|
||||||
|
## What this leaves open
|
||||||
|
|
||||||
|
- The **decision itself**, as a record: the bus is NATS; the AMQP broker becomes the predecessor's
|
||||||
|
compatibility broker and retires with the last AMQP client. Written when the operator says so.
|
||||||
|
- **JetStream's shape for the control plane**: one stream per concern (control, nodes, builds,
|
||||||
|
events) or one with subjects; retention; what the store window guarantee looks like as ack-wait
|
||||||
|
and nak-delay. Measured in the lab, not designed on paper.
|
||||||
|
- **Accounts as configuration**: the controller writes users and permissions into a file the host
|
||||||
|
declares, reloaded on change — which is the [ADR 0102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
|
||||||
|
discipline applied from the start — or the JWT/operator model. The first is simpler and matches
|
||||||
|
how the mesh already writes everything.
|
||||||
|
- **The MCP bridge** the operator asked for the same day is written against the sdk's contract, so
|
||||||
|
it moves with the bus and is not written twice.
|
||||||
|
- Whether a **leaf node per machine** replaces the hub-and-spoke bus later — out of scope here.
|
||||||
Reference in New Issue
Block a user