A seat is handed over as one act, and the holder is on record

`seat <name> --to <node>/<module>` makes one assignment the holder of a seat in
the same write that removes the previous one. The row is new (migration 0039);
without one, the resolver derives the holder as it always did — the sole eligible
assignment, two refused — so nothing changes for a mesh that never hands a seat
over. With one, the recorded assignment holds and any other whose module could
hold the seat is eligible and silent: not refused, not holding. That is what lets
the next holder run beside the current one until the switch (hq design 26, design
28 task 5.3, ADR 0131).

Why: the controller finds its own bus through a seat, and the day that seat was
left with nobody in it — because two eligible holders could not coexist and the
old one's claim was taken away — the control plane looped for two hours while
every service stayed up. A handover that is never empty in between is the fix,
not a workaround for it.

`CanHold` is the one judgement of whether a module may hold a seat — claims it at
its scope, provides what it delivers, against the store's row — shared by
registration and the handover so they cannot drift apart. The holding belongs to
the assignment and goes when it does, so a seat never points at nothing running.

Tests: the resolver with and without a record, on the same and another machine,
under a former name; the store's row replaced not added, refused for an
unassigned target, removed with its assignment; CanHold's four answers and that
they follow the store. Full suite green against a real NATS and store.
This commit is contained in:
2026-09-27 23:22:20 +02:00
parent 4e4481b6f2
commit 585a6abbdd
10 changed files with 438 additions and 27 deletions
+30 -12
View File
@@ -46,21 +46,39 @@ func TestTheSeatRefusesADifferentBusToo(t *testing.T) {
}
}
// The old broker no longer claims the seat: it is an ordinary provider of `amqp`
// (novox/hq ADR 0119), so it can sit on the same mesh as the bus without contending for it.
func TestTheAmqpBrokerDoesNotContendForTheSeat(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")
for _, c := range lavinmq.Claims {
if c.Name == "mesh-broker" {
t.Fatal("the amqp broker still claims mesh-broker; it is a provider, not foundation")
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)
}
}
busHeld := World{Held: []Held{{Claim: "mesh-broker", Scope: ScopeMesh,
Node: "anchor", Module: "nats"}}}
other := workstation()
other.Name = "laptop"
if _, err := Resolve(shelf(lavinmq), []string{"lavinmq"}, other, busHeld); err != nil {
t.Fatalf("the amqp broker was refused beside the mesh bus: %v", err)
if len(holders) != 1 || holders[0] != "nats" {
t.Fatalf("the seat is held by %v, not by the holder on record alone", holders)
}
}