From e02279885811da8340db8b6ba5f30ac81db6728a Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 23 Sep 2026 23:39:17 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200106:=20the=20bus=20is=20NATS=20=E2=80=94?= =?UTF-8?q?=20native,=20built=20beside=20the=20migration,=20cut=20over=20a?= =?UTF-8?q?fter=20its=20core;=20issue=20104=20resolved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../014-the-bus-on-nats/00-overview.md | 3 +- 02-DECISIONS/0106-the-bus-is-nats.md | 83 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/08-connectivity.md | 12 +++ .../00-report.md | 4 +- 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 02-DECISIONS/0106-the-bus-is-nats.md diff --git a/01-RESEARCH/014-the-bus-on-nats/00-overview.md b/01-RESEARCH/014-the-bus-on-nats/00-overview.md index 1613848..bcaa4e1 100644 --- a/01-RESEARCH/014-the-bus-on-nats/00-overview.md +++ b/01-RESEARCH/014-the-bus-on-nats/00-overview.md @@ -1,5 +1,6 @@ --- -status: active +status: graduated +became: 02-DECISIONS/0106-the-bus-is-nats.md initiated: 2026-09-23 touches: - 02-DECISIONS/0002-nodes-communicate-over-a-broker.md diff --git a/02-DECISIONS/0106-the-bus-is-nats.md b/02-DECISIONS/0106-the-bus-is-nats.md new file mode 100644 index 0000000..6da6c77 --- /dev/null +++ b/02-DECISIONS/0106-the-bus-is-nats.md @@ -0,0 +1,83 @@ +--- +topic: the mesh +status: accepted +date: 2026-09-23 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0002-nodes-communicate-over-a-broker.md +--- + +# 106. The bus is NATS + +## Context + +The mesh's bus is an AMQP broker. [Research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md) +measured what that costs and what it would take to change: AMQP is spoken in three places of the +mesh's own code — the controller's link, the host's link, the tool runtime's client — and in none of +the sdk or the modules, because [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) kept the client +out of the sdk for exactly this. The predecessor's world is AMQP and is being retired module by +module; it cannot move and does not need to. + +The operator's direction: NATS, native — the mesh built on it, not bridged to it — and the tools a +person reaches from a workstation designed on it rather than built quickly on what exists. + +## Considered Options + +1. **Stay on AMQP.** Rejected by the operator. +2. **NATS behind a bridge**, the mesh unchanged. Rejected: two buses, every guarantee crossing a + seam, and the reason for changing — one native, simple, subject-addressed bus with request/reply + and accounts built in — lost at the seam. +3. **NATS native, built in the lab in parallel with the migration, cut over in one rehearsed + rollout after the migration's core is done.** Adopted. + +## Decision + +**The mesh's bus is NATS.** Every link the mesh has — control, node queues, builds, enrolment, +reports, events, tool invocation — is carried on NATS subjects; durability, catch-up and +hold-unacknowledged-and-retry ([ADR 0083](0083-one-push-leaves-the-mesh-consistent.md)) are +JetStream's; a module's account is a NATS account with per-subject permissions derived from its +`emits` and `consumes` ([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)), +written as configuration the host declares and the server reloads — never through a management +API. The broker module changes; the seat `mesh-broker` does not +([ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)). + +**The sdk's broker contract does not change.** Modules are written against `request`, `handle`, +`publish`, `subscribe`; the runtime implements them on NATS; no module speaks a protocol. + +**The AMQP broker the mesh adopted becomes the predecessor's compatibility broker**, kept as a +module with one purpose — the predecessor's clients — and retired with the last of them. + +**It is built beside the migration and cut over after its core.** The bus is not changed under a +half-migrated node. A lab bed proves the whole path — enrolment, the store window, upgrades, tool +invocation — on NATS before any node's bus moves, and the move is one rollout: controller, every +host, every runtime together. + +**What a person reaches the mesh's tools with is designed on NATS**, as part of the same design: a +person's account, scoped like a module's, and a client that speaks the bus directly. + +## Consequences + +- The architecture — subjects, streams, accounts, the enrolment handshake, the person's client — is + written as a to-be design before code, and reviewed. +- Eight records are touched: 0002 and 0033 (the substrate names a broker — now NATS), 0041 and + 0042 (events keep their shape; the exchange becomes a subject prefix), 0043 (accounts as + configuration), 0078 (the broker module is `nats`), 0083 (JetStream carries the guarantee), 0039 + (unchanged, and the reason this is possible). +- The lab beds that prove the bus are re-run on NATS; none is skipped. +- Until the cutover, nothing changes on any node. + +## How it is checked + +A lab bed raises a mesh on NATS from genesis and proves: a node enrols over TLS with a claimed +token; a push composes, is held while the store restarts and applies after; an upgrade rolls out; +a module's tools are invoked from another node and from a person's client; an event dead-letters +after its deliveries are exhausted; a module's account cannot publish outside its `emits` nor +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. + +## References + +- [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md) +- [ADR 0002](0002-nodes-communicate-over-a-broker.md), [0033](0033-the-substrate-is-a-store-and-a-broker.md), + [0039](0039-what-the-sdk-holds-and-refuses.md), [0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md), + [0078](0078-the-store-and-broker-are-modules.md), [0083](0083-one-push-leaves-the-mesh-consistent.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index c152888..4b85536 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -94,6 +94,7 @@ python3 00-META/checks/index.py fail if stale - **0103** — [What an adopted node holds, and what its guard refuses](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md) - **0104** — [A provision may be answered by an adapter to the predecessor](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.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) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 28aa8c0..fad77e3 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -10,6 +10,7 @@ code: updated: 2026-09-23 decisions: - 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md + - 02-DECISIONS/0106-the-bus-is-nats.md - 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md - 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md @@ -732,3 +733,14 @@ controller composes follows it, as readers of a setting. ADR 0100's non-overlap where a found tunnel is left running beside the mesh's; where it is adopted there is one tunnel. The guard admits the mesh's ports from that one interface, and the predecessor's peers arrive on it. + +## The bus is NATS + +*2026-09-23, [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md). Architecture to be written +before code; this section names the shape only.* + +Every link the mesh has rides NATS subjects; durability is JetStream's; a module's account is a +NATS account with permissions derived from `emits`/`consumes`, declared as configuration the host +writes and the server reloads. The sdk's contract is unchanged. The adopted AMQP broker stays as +the predecessor's compatibility broker until its last client is gone. Built in the lab beside the +migration; cut over in one rollout after the core; the person's client is designed on it. diff --git a/04-ISSUES/104-reconcile-applies-a-stale-declaration-and-refuses-nothing/00-report.md b/04-ISSUES/104-reconcile-applies-a-stale-declaration-and-refuses-nothing/00-report.md index 085c0ff..c1f08fb 100644 --- a/04-ISSUES/104-reconcile-applies-a-stale-declaration-and-refuses-nothing/00-report.md +++ b/04-ISSUES/104-reconcile-applies-a-stale-declaration-and-refuses-nothing/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: resolved opened: 2026-09-23 located-in: [mesh-host cmd/mesh-host] -fixed-by: +fixed-by: mesh-host — a declaration for the other mode is refused, every file is refused once the mesh has spoken, and an apply previews what it would change amended-design: --- -- 2.54.0