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

Merged
jschoubben merged 1 commits from feat/amqp-is-not-a-provision into main 2026-09-27 21:30:09 +00:00
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
// while it waits** (novox/hq ADR 0131, superseding the record this test used to pin). Whoever is on
// record holds it; the other eligible claimant is neither refused nor holding. This is the shape the
// 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)
}
}
// The old broker is gone from the catalogue (novox/hq ADR 0131, design 28 task 5.4), so it is no
// longer a fixture here. That two eligible holders stand beside each other with one on record is
// pinned in holdings_test.go against manifests this package owns.
// **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-*`
@@ -28,8 +28,10 @@ func TestTheStoreAndTheBrokerSayWhatTheMeshGuards(t *testing.T) {
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)
}
if got := catalogueManifest(t, "lavinmq").Guards; !reflect.DeepEqual(got, []int{15672}) {
t.Errorf("lavinmq guards %v; the management port must be refused from outside", got)
// The bus's monitoring port, not its client port: a node reaches the bus, nobody outside
// 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(
"%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 {
p := offer.Name
if !name.MatchString(p) {
+5 -5
View File
@@ -121,9 +121,9 @@ func TestTheParserDoesNotJudgeWhatOnlyTheStoreKnows(t *testing.T) {
t.Cleanup(func() { UseSeats(was) })
// A store whose bus seat delivers something this module does provide.
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "amqp", Decision: "test"}})
raw := []byte(`{"module":"lavinmq","version":"1",` +
`"provides":[{"name":"amqp","scope":"mesh"}],` +
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"}})
raw := []byte(`{"module":"a-bus","version":"1",` +
`"provides":[{"name":"mesh-bus","scope":"mesh"}],` +
`"claims":[{"name":"mesh-broker","scope":"mesh"}]}`)
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.
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 {
t.Fatalf("the parser judged it the second time: %v", err)
}
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)
}
}
@@ -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';