From d1a6950071e8f0b46d1e3ced73742a4504f574c5 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 00:31:27 +0100 Subject: [PATCH] The assigner's check says a seat the unread seat list does not name waits for the list, never held once (issue 1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A mesh seat held on two nodes was refused as held once whenever catalogue did not name it, and a host that has not read the seat list holds only the bootstrapped definitions: mesh-dns-resolver, a replicated seat, refused every proposal (ADR 0636 §1). Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- .../internal/assign/seat.go | 5 +++- .../internal/assignment/assignments_test.go | 30 ++++++++++++++----- .../internal/assignment/check.go | 22 ++++++++++++-- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/modules/mesh-delivery-assigner/internal/assign/seat.go b/modules/mesh-delivery-assigner/internal/assign/seat.go index 7e7fde86..48b6ee28 100644 --- a/modules/mesh-delivery-assigner/internal/assign/seat.go +++ b/modules/mesh-delivery-assigner/internal/assign/seat.go @@ -36,6 +36,9 @@ type ProposeIn struct { Recorded func(node string) bool // Warrant is the enrolment's warrant an add-node carries (ADR 0587 §7); only that change has one. Warrant *mma.Warrant + // SeatListRead says the host has read the seat list from the seat mesh-catalog (ADR 0636 §1); until then a mesh seat + // the bootstrapped definitions do not name is not judged held once (plc.CheckAssignments). + SeatListRead bool } // Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision), @@ -110,7 +113,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Assignments, []string) was := recorded recorded = func(node string) bool { return joining[node] || was(node) } } - for _, r := range plc.CheckAssignments(all, held, recorded) { + for _, r := range plc.CheckAssignments(all, held, recorded, in.SeatListRead) { findings = append(findings, r) } return p, out, findings diff --git a/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go b/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go index 920c649e..13789020 100644 --- a/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go +++ b/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go @@ -67,17 +67,31 @@ func TestAssignments(t *testing.T) { t.Run("the check of every assignments", func(t *testing.T) { known := func(m string) bool { return m != "ghost" } enrolled := func(n string) bool { return n != "stranger" } - if r := CheckAssignments(current, known, enrolled); len(r) != 0 { + if r := CheckAssignments(current, known, enrolled, true); len(r) != 0 { t.Fatalf("a sound assignments is refused: %v", r) } - // A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there. - for name, refused := range map[string]bool{"mesh-dns-resolver": false, "mesh-broker": true} { - a := seat.Assignments{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: name, Scope: "mesh", Module: "dns"}}} + // A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there. Issue 1966: + // a seat the list does not name, while the list is unread, waits for it and is never said held once (ADR 0636 §1); + // once read, it is judged held once. + for _, c := range []struct { + seat string + listRead bool + want string + }{ + {"mesh-dns-resolver", true, ""}, + {"mesh-dns-resolver", false, ""}, + {"mesh-broker", true, "held once for the mesh, is held on ace and novox"}, + {"mesh-broker", false, "held once for the mesh, is held on ace and novox"}, + {"a-seat-not-listed", false, "the seat list is not read yet, so whether the seat a-seat-not-listed may be held on ace and novox waits for it"}, + {"a-seat-not-listed", true, "held once for the mesh, is held on ace and novox"}, + } { + a := seat.Assignments{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: c.seat, Scope: "mesh", Module: "dns"}}} b := a b.Node = "novox" - r := CheckAssignments(map[string]seat.Assignments{"ace": a, "novox": b}, known, enrolled) - if (len(r) != 0) != refused { - t.Errorf("%s on two nodes: refusals %v, want refused %v", name, r, refused) + r := CheckAssignments(map[string]seat.Assignments{"ace": a, "novox": b}, known, enrolled, c.listRead) + got := strings.Join(r, "; ") + if (c.want == "") != (len(r) == 0) || !strings.Contains(got, c.want) || (!c.listRead && c.seat == "a-seat-not-listed" && strings.Contains(got, "held once")) { + t.Errorf("%s on two nodes, the list read %v: refusals %v, want %q", c.seat, c.listRead, r, c.want) } } clash := g14 @@ -89,7 +103,7 @@ func TestAssignments(t *testing.T) { twice := novox twice.Node = "stranger" twice.Decided.Address = "10.77.0.2" - r := CheckAssignments(map[string]seat.Assignments{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled) + r := CheckAssignments(map[string]seat.Assignments{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled, true) for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080", "bound to ace/postgres", "pin of shell names g14/bash", "stranger is a node with no record", "mesh-store, held once for the mesh, is held on novox and stranger", diff --git a/modules/mesh-delivery-assigner/internal/assignment/check.go b/modules/mesh-delivery-assigner/internal/assignment/check.go index a3151d9f..fdd4a9cb 100644 --- a/modules/mesh-delivery-assigner/internal/assignment/check.go +++ b/modules/mesh-delivery-assigner/internal/assignment/check.go @@ -33,7 +33,13 @@ var KeptApart = []struct{ A, B, Record string }{ // (catalogue.Seat's Replicated, ADR 0223), which is held once per node; two nodes with one address on the private // network; more than one hub; two modules KeptApart on one node. A host port a manifest declares without a number and a // requirement left unbound are the composition's to find (its ForeseenPorts and Unbound). -func CheckAssignments(assignments map[string]seat.Assignments, known func(module string) bool, enrolled func(node string) bool) []string { +// +// **A seat the list does not name is not judged held once while the list is unread** (novox/hq ADR 0636 §1, issue 1966): +// listRead says the host has read the seat list from the seat mesh-catalog; before, catalogue holds the bootstrapped +// definitions alone, and a mesh seat it does not name, held on two nodes, may be a replicated one. Its refusal then says the +// judgement waits for the list (SeatListWaits), never that it is held twice; once the list is read, a seat it does not name +// is judged as a seat held once, as before. +func CheckAssignments(assignments map[string]seat.Assignments, known func(module string) bool, enrolled func(node string) bool, listRead bool) []string { var refusals []string nodes := sortedKeys(assignments) placed := func(p seat.Provider) bool { @@ -105,13 +111,18 @@ func CheckAssignments(assignments map[string]seat.Assignments, known func(module continue } name := h.Claim + "/" + h.Kind - if s, known := catalogue.SeatNamed(h.Claim); known && s.Replicated { + s, defined := catalogue.SeatNamed(h.Claim) + if defined && s.Replicated { // A replicated mesh seat (ADR 0223) is held on several nodes at once, each answering the same thing; // it is still held once on each node. name += "/" + node } if other, twice := meshSeats[name]; twice { - refusals = append(refusals, fmt.Sprintf("the seat %s, held once for the mesh, is held on %s and %s", h.Claim, other, node)) + if !defined && !listRead { + refusals = append(refusals, fmt.Sprintf(SeatListWaits, h.Claim, other, node)) + } else { + refusals = append(refusals, fmt.Sprintf("the seat %s, held once for the mesh, is held on %s and %s", h.Claim, other, node)) + } } meshSeats[name] = node } @@ -122,6 +133,11 @@ func CheckAssignments(assignments map[string]seat.Assignments, known func(module return refusals } +// SeatListWaits is the refusal of a mesh seat held on two nodes that the unread seat list does not name (ADR 0636 §1): the +// seat, and the two nodes. +const SeatListWaits = "the seat list is not read yet, so whether the seat %s may be held on %s and %s waits for it: " + + "the assigner judges it once it has read the list from the seat mesh-catalog (novox/hq ADR 0636 §1)" + // Change is what `assignments-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had // none) and after (0: the key was deleted), and its modules added, removed or changed against the assignments before. type Change struct {