The assigner's check says a seat the unread seat list does not name waits for the list, never held once (issue 1966)

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
This commit is contained in:
2026-11-09 00:31:27 +01:00
parent 4f9b15a43f
commit d1a6950071
3 changed files with 45 additions and 12 deletions
@@ -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
@@ -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",
@@ -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 {