AMQP is not a provision: the bus seat delivers the bus, and the word is refused

Two halves of novox/hq ADR 0131. Migration 0040 moves the mesh-broker row from
`amqp` to `mesh-bus`, so the seat's holder answers for the mesh's bus and not for
the wire protocol the old broker spoke — which is what let only the retiring
broker hold the seat that names the bus. Safe under the current holder: the
control plane composes its own address through the seat by name and the overview
derives holders by name; only registration and provision resolution read the
column. What must not happen in between is re-registering the current holder.

And the parser refuses a manifest that provides or requires `amqp`, each refusal
saying what to do instead: a module reaches the mesh's bus through the sdk and
depends on the seat, not on a protocol. A whole-catalogue test asserts nothing
beside this checkout names it; the three modules that did are removed there.

Two tests that used the old broker as a fixture now use the module that replaces
it or a manifest this package owns.
This commit is contained in:
2026-09-27 23:29:00 +02:00
parent 33c4e4be34
commit f325073982
6 changed files with 94 additions and 42 deletions
@@ -0,0 +1,48 @@
package catalogue
import (
"os"
"path/filepath"
"strings"
"testing"
)
// **The word does not come back through a manifest** (novox/hq ADR 0131). A module that wants
// messaging wants the mesh's bus, reached through the sdk and named by the `mesh-broker` seat. Naming
// the old wire protocol asks for the one server being retired, so both directions are refused at the
// parser — this is judged from the manifest alone, no store needed.
func TestAManifestProvidingAmqpIsRefused(t *testing.T) {
raw := []byte(`{"module":"old-broker","version":"1","provides":[{"name":"amqp","scope":"mesh"}]}`)
_, err := ParseManifest(raw)
if err == nil || !strings.Contains(err.Error(), `provides "amqp", which is not a provision`) {
t.Fatalf("a module providing amqp was not refused, or not for the reason: %v", err)
}
}
func TestAManifestRequiringAmqpIsRefused(t *testing.T) {
raw := []byte(`{"module":"forwarder","version":"1","requires":["amqp"]}`)
_, err := ParseManifest(raw)
if err == nil || !strings.Contains(err.Error(), `requires "amqp", which is not a provision`) {
t.Fatalf("a module requiring amqp was not refused, or not for the reason: %v", err)
}
}
// And the catalogue as checked out beside this repository names it nowhere — the three modules that
// did are removed under design 28 task 5.4, not converted.
func TestNoCatalogueManifestNamesAmqp(t *testing.T) {
modules, err := filepath.Glob("../../../mesh-catalog/modules/*/module.json")
if err != nil || len(modules) == 0 {
t.Skip("the catalogue is not checked out beside this repository")
}
for _, path := range modules {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), `"amqp"`) {
t.Errorf("%s names amqp, which is not a provision (novox/hq ADR 0131)",
filepath.Base(filepath.Dir(path)))
}
}
}
+3 -35
View File
@@ -46,41 +46,9 @@ func TestTheSeatRefusesADifferentBusToo(t *testing.T) {
} }
} }
// **The old broker claims the seat until the seat is handed over, and stands beside the new one // The old broker is gone from the catalogue (novox/hq ADR 0131, design 28 task 5.4), so it is no
// while it waits** (novox/hq ADR 0131, superseding the record this test used to pin). Whoever is on // longer a fixture here. That two eligible holders stand beside each other with one on record is
// record holds it; the other eligible claimant is neither refused nor holding. This is the shape the // pinned in holdings_test.go against manifests this package owns.
// handover needs: both brokers assigned, one bus, no moment with nobody in the seat.
func TestTheOldBrokerStandsBesideTheNewOneUntilTheHandover(t *testing.T) {
was := Seats()
t.Cleanup(func() { UseSeats(was) })
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"}})
lavinmq := catalogueManifest(t, "lavinmq")
if !lavinmq.ClaimsSeat("mesh-broker") {
t.Skip("the old broker no longer claims the seat: design 28 task 5.4 has removed it")
}
nats := catalogueManifest(t, "nats")
onRecord := World{Holdings: []Held{{Claim: "mesh-broker", Scope: ScopeMesh,
Node: "anchor", Module: "nats"}}}
// The same machine runs both. Without the record this is two holders and refused; with it, the
// recorded one holds and the other is silent.
anchor := workstation()
anchor.Name = "anchor"
got, err := Resolve(shelf(lavinmq, nats), []string{"lavinmq", "nats"}, anchor, onRecord)
if err != nil {
t.Fatalf("the old broker beside the recorded holder was refused: %v", err)
}
var holders []string
for _, h := range got.Claims {
if h.Claim == "mesh-broker" {
holders = append(holders, h.Module)
}
}
if len(holders) != 1 || holders[0] != "nats" {
t.Fatalf("the seat is held by %v, not by the holder on record alone", holders)
}
}
// **A seat and the interface it delivers are different names, and renaming one must not rename // **A seat and the interface it delivers are different names, and renaming one must not rename
// the other** (novox/hq ADR 0118). This nearly went wrong: the seats were renamed to the `mesh-*` // the other** (novox/hq ADR 0118). This nearly went wrong: the seats were renamed to the `mesh-*`
@@ -28,8 +28,10 @@ func TestTheStoreAndTheBrokerSayWhatTheMeshGuards(t *testing.T) {
if got := catalogueManifest(t, "postgres").Guards; !reflect.DeepEqual(got, []int{5432}) { if got := catalogueManifest(t, "postgres").Guards; !reflect.DeepEqual(got, []int{5432}) {
t.Errorf("postgres guards %v; the store's port must be refused from outside", got) t.Errorf("postgres guards %v; the store's port must be refused from outside", got)
} }
if got := catalogueManifest(t, "lavinmq").Guards; !reflect.DeepEqual(got, []int{15672}) { // The bus's monitoring port, not its client port: a node reaches the bus, nobody outside
t.Errorf("lavinmq guards %v; the management port must be refused from outside", got) // reads its state (novox/hq ADR 0131 — the broker that guarded 15672 has left the catalogue).
if got := catalogueManifest(t, "nats").Guards; !reflect.DeepEqual(got, []int{8222}) {
t.Errorf("nats guards %v; the monitoring port must be refused from outside", got)
} }
} }
+22
View File
@@ -1027,6 +1027,28 @@ func ParseManifest(raw []byte) (Manifest, error) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%q is not a usable slug: lower-case letters, digits, dashes and dots", m.Slug)) "%q is not a usable slug: lower-case letters, digits, dashes and dots", m.Slug))
} }
// **`amqp` is not a provision, and not a requirement** (novox/hq ADR 0131). A module that wants
// messaging wants the mesh's bus — it emits and consumes through the sdk, which the mesh hands the
// bus with the module's own credential — and the bus is whatever holds `mesh-broker`, spoken in
// whatever that holder speaks. Naming the old wire protocol asks for a specific server, and the
// only one that could answer is the one being retired. Refused here so the word cannot come back
// through a manifest.
for _, offer := range m.Provides {
if offer.Name == "amqp" {
problems = append(problems, fmt.Sprintf(
"%s provides %q, which is not a provision: the mesh's bus is whatever holds "+
"mesh-broker, and a module provides mesh-bus to be it (novox/hq ADR 0131)",
m.Module, offer.Name))
}
}
for _, r := range m.Requires {
if r == "amqp" {
problems = append(problems, fmt.Sprintf(
"%s requires %q, which is not a provision: a module reaches the mesh's bus through "+
"the sdk, and depends on the mesh-broker seat, not on a protocol (novox/hq ADR 0131)",
m.Module, r))
}
}
for _, offer := range m.Provides { for _, offer := range m.Provides {
p := offer.Name p := offer.Name
if !name.MatchString(p) { if !name.MatchString(p) {
+5 -5
View File
@@ -121,9 +121,9 @@ func TestTheParserDoesNotJudgeWhatOnlyTheStoreKnows(t *testing.T) {
t.Cleanup(func() { UseSeats(was) }) t.Cleanup(func() { UseSeats(was) })
// A store whose bus seat delivers something this module does provide. // A store whose bus seat delivers something this module does provide.
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "amqp", Decision: "test"}}) UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"}})
raw := []byte(`{"module":"lavinmq","version":"1",` + raw := []byte(`{"module":"a-bus","version":"1",` +
`"provides":[{"name":"amqp","scope":"mesh"}],` + `"provides":[{"name":"mesh-bus","scope":"mesh"}],` +
`"claims":[{"name":"mesh-broker","scope":"mesh"}]}`) `"claims":[{"name":"mesh-broker","scope":"mesh"}]}`)
m, err := ParseManifest(raw) m, err := ParseManifest(raw)
@@ -135,12 +135,12 @@ func TestTheParserDoesNotJudgeWhatOnlyTheStoreKnows(t *testing.T) {
} }
// And with the store saying the seat delivers something else, registration is what refuses it. // And with the store saying the seat delivers something else, registration is what refuses it.
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"}}) UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "other-bus", Decision: "test"}})
if _, err := ParseManifest(raw); err != nil { if _, err := ParseManifest(raw); err != nil {
t.Fatalf("the parser judged it the second time: %v", err) t.Fatalf("the parser judged it the second time: %v", err)
} }
got := strings.Join(CatalogueProblems(Shelf{m.Module: m}), "; ") got := strings.Join(CatalogueProblems(Shelf{m.Module: m}), "; ")
if !strings.Contains(got, `does not provide "mesh-bus"`) { if !strings.Contains(got, `does not provide "other-bus"`) {
t.Fatalf("registration did not refuse a holder that cannot answer for the seat: %q", got) t.Fatalf("registration did not refuse a holder that cannot answer for the seat: %q", got)
} }
} }
@@ -0,0 +1,12 @@
-- The bus seat's holder answers for the mesh's bus, not for a wire protocol (novox/hq ADR 0131).
--
-- The row said `amqp`, which is the protocol the old broker spoke, and so only that broker could hold
-- the seat that names the mesh's bus — while the module that will carry the bus could not. The seat
-- delivers `mesh-bus`; whichever module provides that may hold it, and today that is one module.
--
-- Safe under the current holder: the control plane composes its own bus address through the seat by
-- name, and the overview derives holders by name. Only registration and provision-to-seat resolution
-- read this column. So the row changes, the current holder keeps holding by derivation, the next one
-- can register its claim, and the handover (0039) moves the seat when both are running. What must
-- not happen in between is re-registering the current holder — registration would now refuse it.
update seat set delivers = 'mesh-bus' where name = 'mesh-broker' and delivers = 'amqp';