`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.
120 lines
5.5 KiB
Go
120 lines
5.5 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The mesh's bus is one per mesh, read from the catalogue beside this checkout.
|
|
//
|
|
// **This is step 2's claim, and it is checked here rather than in a bed** (novox/hq ADR 0116):
|
|
// adoption puts the NATS server into the `mesh-broker` seat on a mesh that is already running,
|
|
// and the property that matters is that a second one anywhere is refused *when it is assigned*,
|
|
// not discovered later as two servers holding different halves of the mesh's traffic. A second
|
|
// bus is not a degraded mesh; it is two meshes that both believe they are the one.
|
|
func TestASecondMeshBusAnywhereIsRefusedByName(t *testing.T) {
|
|
nats := catalogueManifest(t, "nats")
|
|
|
|
if _, err := Resolve(shelf(nats), []string{"nats"}, workstation(), World{}); err != nil {
|
|
t.Fatalf("the bus alone does not resolve: %v", err)
|
|
}
|
|
|
|
elsewhere := World{Held: []Held{{Claim: "mesh-broker", Scope: ScopeMesh,
|
|
Node: "anchor", Module: "nats"}}}
|
|
other := workstation()
|
|
other.Name = "laptop"
|
|
_, err := Resolve(shelf(nats), []string{"nats"}, other, elsewhere)
|
|
if err == nil {
|
|
t.Fatal("a second bus was accepted on another machine")
|
|
}
|
|
if !strings.Contains(err.Error(), "mesh-broker") || !strings.Contains(err.Error(), "one per mesh") {
|
|
t.Fatalf("refused without naming the seat: %v", err)
|
|
}
|
|
}
|
|
|
|
// The seat is the server's role, not the product's name (novox/hq ADR 0079). A different
|
|
// implementation of the bus claims the same seat, and the mesh refuses it for the same reason —
|
|
// which is the property that lets the bus be replaced at all.
|
|
func TestTheSeatRefusesADifferentBusToo(t *testing.T) {
|
|
nats := catalogueManifest(t, "nats")
|
|
held := World{Held: []Held{{Claim: "mesh-broker", Scope: ScopeMesh,
|
|
Node: "anchor", Module: "some-other-broker"}}}
|
|
other := workstation()
|
|
other.Name = "laptop"
|
|
if _, err := Resolve(shelf(nats), []string{"nats"}, other, held); err == nil {
|
|
t.Fatal("the seat admitted a second holder because the module's name differed")
|
|
}
|
|
}
|
|
|
|
// **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)
|
|
}
|
|
}
|
|
|
|
// **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-*`
|
|
// prefix, and a blanket search-and-replace also renamed `npm-package-registry` and `git` where
|
|
// they are *provisions* — which a consumer requires and a provider offers. The tests failed with
|
|
// "the package registry is served on <nil>", which does not say "you renamed an interface".
|
|
func TestRenamingASeatDidNotRenameTheInterfaceItDelivers(t *testing.T) {
|
|
for _, pair := range []struct{ seat, delivers string }{
|
|
{"git", "git"},
|
|
{"npm-package-registry", "npm-package-registry"},
|
|
{"the-artifact-store", "artifact-store"},
|
|
{"mesh-store", "postgres-database"},
|
|
{"mesh-broker", "mesh-bus"},
|
|
} {
|
|
s, known := SeatNamed(pair.seat)
|
|
if !known {
|
|
t.Fatalf("%q is not a seat", pair.seat)
|
|
}
|
|
if s.Delivers != pair.delivers {
|
|
t.Errorf("the %s seat delivers %q, expected %q — renaming the seat moved the "+
|
|
"interface with it, and every consumer requiring it would stop resolving",
|
|
pair.seat, s.Delivers, pair.delivers)
|
|
}
|
|
// **Three of these deliberately share a name with what they deliver**, and that is not an
|
|
// incomplete rename. Renaming a seat that delivers a provision cascades to every consumer
|
|
// requiring it, with a mesh-wide window where a holder stops resolving mid-flight — so the
|
|
// trunk deferred exactly those three (novox/hq ADR 0121) while renaming the node-scoped ones.
|
|
// What this test is for is the other direction: that renaming a seat never moves the
|
|
// interface, which once produced "the package registry is served on <nil>".
|
|
}
|
|
}
|
|
|
|
// A manifest written against an old seat name is told what it became rather than refused as
|
|
// unknown. **That map is the controller's store now, not this package** (novox/hq ADR 0122): a
|
|
// rename is a row, so the courtesy survives a rename nobody recompiled for. Checked where the
|
|
// table is read, not here, where there is no longer a hardcoded list to check against.
|