From 00817fb9e39c220121b386612979acb28045a2c6 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 00:06:15 +0200 Subject: [PATCH] Design 25: the store window, and what moving it into the server changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guarantee is the same and the mechanism is simpler — a nak with a delay, no parked list, nothing lost when the controller restarts. It costs one thing: a naked message comes back whatever happened meanwhile, so an older report is redelivered after a newer was applied. A report already carries the digest of the declaration it answers, so supersession becomes a check rather than memory — ordering settled by what a message says, not by when it arrived. --- 03-DESIGN/01-to-be/25-the-bus-on-nats.md | 27 +++++++++++++++++++++++ 03-DESIGN/01-to-be/28-building-the-bus.md | 13 +++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) 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 0cf7128..4100103 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 @@ -128,6 +128,33 @@ call is a timeout the caller already handles. Streams and consumers are objects the controller creates at genesis and asserts on start; a module declares nothing about them. The controller is the only writer of stream definitions. +### The store window, and what moving it into the server changes + +The guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)) is +that a push the controller cannot record because its store is restarting is **held and retried** — +never dropped, never falsely acknowledged. Here that is a `nak` with a delay: the server holds +the message and redelivers it, so the controller keeps no list of parked messages and one that +restarts mid-window loses nothing it was holding. + +**That is a plain win, and it introduces one problem worth naming.** Holding a delivery in memory +let the controller drop an older report when a newer one for the same node arrived, because +acting on the older after the newer would undo the newer. A `nak`ed message belongs to the server +and comes back whatever happened meanwhile — so the older report is redelivered *after* the newer +was applied. + +The answer was already in the message. A report carries the **digest of the declaration it is +about**, which exists because an earlier attempt to order reports by time lost the race it +invited: an apply that began under the previous declaration finishes after the next is sent, and +its report reads as newer than the send. Clocks cannot answer *which*. + +So supersession stops being something the controller remembers and becomes something it checks — +a report whose digest is not the one outstanding for that node is acknowledged without being +acted on. The same shape as a node refusing a superseded declaration by sequence +([issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md)): **ordering settled +by what a message says, not by when it arrived.** And staleness is checked before the store is +waited on, so a redelivery that lost its race does not hold a slot in the window that a current +message needs. + ## 4. Accounts [ADR 0043](../../02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) says 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 53ab087..ac42cfb 100644 --- a/03-DESIGN/01-to-be/28-building-the-bus.md +++ b/03-DESIGN/01-to-be/28-building-the-bus.md @@ -250,8 +250,17 @@ pays for itself furthest away. rather than from the code that wrote it. The seam turned out to be eight call sites — the same smallness that said this bus could be - replaced at all. Still on `*amqp.Channel`: `RequestBuild` and `Ask`, which carry reply-queue - machinery, and the whole consume side (the control loop, enrolment, serving). + replaced at all. + + **The consume side's hard part is decided and tested**: the store window is a `nak` with a + delay rather than a delivery held in memory, which also means a controller restarting + mid-window loses nothing. Moving the holding into the server costs one thing — an older + report is redelivered after a newer was applied — and a report already carries the digest + of the declaration it is about, so supersession becomes a check rather than something the + controller remembers. Pure and tested without a bus, a store or a clock. + + Still outstanding: wiring that decision into the loop, `RequestBuild` and `Ask`, enrolment, + and serving. - [~] 3.5 the host's link on NATS — **the outbound half is through a seam**, mirroring the controller's and still importing nothing of the mesh's own (ADR 0005): the host's own interface over its own libraries, agreeing with the controller only because a fixture holds