From f873c97db51e72f643ad4d847c0f9ca007515055 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 23:30:27 +0200 Subject: [PATCH] Issue only the recorded holder a seat held once for the mesh (novox/hq issue 218) A module claiming a mesh-scoped seat was granted and issued the seat's subjects on every machine it runs on, so the store's verbs answered from whichever postgres replied first. Where the mesh records the seat's holder, only that (node, module) is now issued it; the module's own tools are untouched everywhere. --- internal/inventory/busrecords.go | 40 ++++++++++++++++++++++++++++- internal/inventory/heldhere_test.go | 32 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 internal/inventory/heldhere_test.go diff --git a/internal/inventory/busrecords.go b/internal/inventory/busrecords.go index 3fa241b..431e5cc 100644 --- a/internal/inventory/busrecords.go +++ b/internal/inventory/busrecords.go @@ -49,6 +49,12 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) { } } + // Who holds each seat held once for the mesh, where the mesh recorded it (novox/hq issue 218). + holdings, err := i.Holdings(ctx) + if err != nil { + return broker.Records{}, fmt.Errorf("cannot read who holds the mesh's seats: %w", err) + } + out := broker.Records{Assigned: map[string][]broker.Declared{}, People: map[string][]string{}, Interchangeable: map[string]bool{}} for _, n := range nodes { @@ -72,7 +78,9 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) { "%s is assigned to %s and is not in the catalogue, so what it may say cannot "+ "be derived", module, n.Name) } - out.Assigned[n.Name] = append(out.Assigned[n.Name], declaredFor(m, seats)) + d := declaredFor(m, seats) + d.Holds = heldHere(d.Holds, holdings, n.Name, module) + out.Assigned[n.Name] = append(out.Assigned[n.Name], d) if m.Instances == catalogue.InstancesInterchangeable { out.Interchangeable[m.Module] = true } @@ -183,3 +191,33 @@ func (i *Inventory) NodesWithALiveToken(ctx context.Context) ([]string, error) { } return out, rows.Err() } + +// heldHere keeps of what a module claims only the seats it holds on this machine (novox/hq issue 218). +// A seat held once per machine is held by every assignment that claims it. A seat held once for the +// mesh is held by one assignment: where the mesh recorded who holds it, a claim on any other machine +// grants nothing and issues nothing — or the module would serve the role's verbs from a machine that +// is not the role's, and a question to the mesh's store would be answered from the wrong database. A +// mesh seat with no holder on record is left as it was derived. +func heldHere(claimed []broker.Seat, holdings []catalogue.Held, node, module string) []broker.Seat { + recorded := map[string][]catalogue.Held{} + for _, h := range holdings { + if h.Scope == catalogue.ScopeMesh { + recorded[h.Claim] = append(recorded[h.Claim], h) + } + } + var out []broker.Seat + for _, s := range claimed { + holders, onRecord := recorded[s.Name] + if s.Scope != catalogue.ScopeMesh || !onRecord { + out = append(out, s) + continue + } + for _, h := range holders { + if h.Node == node && h.Module == module { + out = append(out, s) + break + } + } + } + return out +} diff --git a/internal/inventory/heldhere_test.go b/internal/inventory/heldhere_test.go new file mode 100644 index 0000000..831208c --- /dev/null +++ b/internal/inventory/heldhere_test.go @@ -0,0 +1,32 @@ +package inventory + +import ( + "testing" + + "github.com/novox/mesh-controller/internal/broker" + "github.com/novox/mesh-controller/internal/catalogue" +) + +// novox/hq issue 218: a seat held once for the mesh is granted and issued only to the holder on record; +// a node seat to every machine's claimant; a mesh seat with no holder on record as derived. +func TestOnlyTheRecordedHolderHoldsAMeshSeat(t *testing.T) { + claimed := []broker.Seat{ + {Name: "mesh-store", Scope: catalogue.ScopeMesh}, + {Name: "node-packet-filter", Scope: catalogue.ScopeNode}, + {Name: "unrecorded", Scope: catalogue.ScopeMesh}, + } + holdings := []catalogue.Held{{Claim: "mesh-store", Scope: catalogue.ScopeMesh, Node: "control", Module: "postgres"}} + names := func(ss []broker.Seat) (out []string) { + for _, s := range ss { + out = append(out, s.Name) + } + return + } + if got := names(heldHere(claimed, holdings, "control", "postgres")); len(got) != 3 { + t.Errorf("the holder lost a seat: %v", got) + } + got := names(heldHere(claimed, holdings, "other", "postgres")) + if len(got) != 2 || got[0] != "node-packet-filter" || got[1] != "unrecorded" { + t.Errorf("a claimant on another machine holds %v; want the node seat and the unrecorded one, not the store", got) + } +}