Move SeatsToPassOn into the SDK and widen it: a holder whose manifest no longer claims its seat has left it (issue 1966, ADR 0660)
Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
@@ -1,14 +1,97 @@
|
||||
package catalogue
|
||||
|
||||
// An assignment as the passing of a seat reads it: a module on a machine, with its manifest.
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// An assignment as the passing of a seat reads it: a module on a machine, with the manifest of the build that machine
|
||||
// runs, else the trunk's (novox/hq ADR 0660 §2).
|
||||
type Assignment struct {
|
||||
Node string
|
||||
Manifest Manifest
|
||||
// Unread is an assignment whose manifest could not be read at all: it is still assigned, so its holdings stand,
|
||||
// and it claims nothing.
|
||||
Unread bool
|
||||
}
|
||||
|
||||
// SeatPass is one seat whose holder on record is assigned nowhere, and the only assignment that can take it.
|
||||
// SeatPass is one seat whose holder on record has left it, and the only assignment that can take it.
|
||||
type SeatPass struct {
|
||||
Seat Seat
|
||||
From Held
|
||||
To Held
|
||||
}
|
||||
|
||||
// SeatsToPassOn reads the mesh for seats held once whose holder on record has left (novox/hq ADR 0449 §4, ADR 0660).
|
||||
//
|
||||
// **A holder on record that is assigned on no machine, or whose every assignment's manifest no longer claims the seat
|
||||
// (or claims it and cannot hold it, by CanHold), holds nothing.** Of such a seat, exactly one assignment in the mesh
|
||||
// able to hold it takes it over: that is a pass, and the caller applies it and says it. An assignment is a module on
|
||||
// one node, so one module placed on two nodes is two claimants. With no such assignment the record stays; with two or
|
||||
// more nothing is passed and the answer says so, naming two and the `seat --to` that settles it. A holder with an
|
||||
// assignment whose manifest still claims the seat changes nothing (ADR 0131), and so does one whose manifest could not
|
||||
// be read. Only mesh seats held once are read: a replicated seat's holders are added by an act (ADR 0223), a kinded
|
||||
// bench is held by its claims, and a seat with several holders on record is a store that disagrees with the mesh.
|
||||
// Nothing here records a pass (ADR 0660 §4).
|
||||
func SeatsToPassOn(holdings []Held, assignments []Assignment) ([]SeatPass, []string) {
|
||||
bySeat := map[string][]Held{}
|
||||
var order []string
|
||||
for _, h := range holdings {
|
||||
s, ok := SeatNamed(h.Claim)
|
||||
if !ok || s.Replicated || s.Scope != ScopeMesh || h.Scope != ScopeMesh || KindedBenches[s.Name] {
|
||||
continue
|
||||
}
|
||||
if _, seen := bySeat[s.Name]; !seen {
|
||||
order = append(order, s.Name)
|
||||
}
|
||||
bySeat[s.Name] = append(bySeat[s.Name], h)
|
||||
}
|
||||
sort.Strings(order)
|
||||
|
||||
var passes []SeatPass
|
||||
var said []string
|
||||
for _, name := range order {
|
||||
recs := bySeat[name]
|
||||
if len(recs) != 1 {
|
||||
continue
|
||||
}
|
||||
seat, _ := SeatNamed(name)
|
||||
holds := false
|
||||
for _, a := range assignments {
|
||||
if a.Manifest.Module != recs[0].Module {
|
||||
continue
|
||||
}
|
||||
if a.Unread || CanHold(a.Manifest, seat) == nil {
|
||||
holds = true
|
||||
}
|
||||
}
|
||||
if holds {
|
||||
continue
|
||||
}
|
||||
var claimants []Held
|
||||
for _, a := range assignments {
|
||||
if a.Unread || CanHold(a.Manifest, seat) != nil {
|
||||
continue
|
||||
}
|
||||
claimants = append(claimants, Held{Claim: seat.Name, Scope: seat.Scope, Node: a.Node, Module: a.Manifest.Module})
|
||||
}
|
||||
sort.Slice(claimants, func(i, j int) bool {
|
||||
if claimants[i].Node != claimants[j].Node {
|
||||
return claimants[i].Node < claimants[j].Node
|
||||
}
|
||||
return claimants[i].Module < claimants[j].Module
|
||||
})
|
||||
switch len(claimants) {
|
||||
case 0:
|
||||
case 1:
|
||||
passes = append(passes, SeatPass{Seat: seat, From: recs[0], To: claimants[0]})
|
||||
default:
|
||||
said = append(said, fmt.Sprintf(
|
||||
"%s on %s is on record as the holder of %q and holds it nowhere, and %s on %s and %s on %s both claim it "+
|
||||
"— nothing is passed; `seat %s --to <node>/<module>` records the one that holds it",
|
||||
recs[0].Module, recs[0].Node, seat.Name, claimants[0].Module, claimants[0].Node,
|
||||
claimants[1].Module, claimants[1].Node, seat.Name))
|
||||
}
|
||||
}
|
||||
return passes, said
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func passSeat(t *testing.T) {
|
||||
t.Helper()
|
||||
was := Seats()
|
||||
t.Cleanup(func() { UseSeats(was) })
|
||||
UseSeats([]Seat{
|
||||
{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"},
|
||||
{Name: "mesh-dns-resolver", Scope: ScopeMesh, Replicated: true, Decision: "test"},
|
||||
})
|
||||
}
|
||||
|
||||
func broker(module string) Manifest {
|
||||
return Manifest{Module: module, Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}},
|
||||
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}
|
||||
}
|
||||
|
||||
// A module that still runs but no longer claims the seat.
|
||||
func dropped(module string) Manifest { return Manifest{Module: module} }
|
||||
|
||||
func recorded(node, module string) []Held {
|
||||
return []Held{{Claim: "mesh-broker", Scope: ScopeMesh, Node: node, Module: module}}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0660 "How it is checked", and ADR 0449 §4's original case.
|
||||
func TestSeatsToPassOn(t *testing.T) {
|
||||
passSeat(t)
|
||||
rec := recorded("novox", "old")
|
||||
cases := []struct {
|
||||
name string
|
||||
rec []Held
|
||||
as []Assignment
|
||||
to string // "node/module" of the one pass, "" for none
|
||||
saidW []string
|
||||
}{
|
||||
{"holder assigned nowhere, one claimant (ADR 0449 §4)", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: broker("new")}}, "novox/new", nil},
|
||||
{"holder assigned but its manifest dropped the claim", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "novox", Manifest: broker("new")}}, "novox/new", nil},
|
||||
{"holder claims but cannot hold the seat", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: Manifest{Module: "old", Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}},
|
||||
{Node: "ace", Manifest: broker("new")}}, "ace/new", nil},
|
||||
{"the running build still claims: the holder keeps the seat", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: broker("old")}, {Node: "novox", Manifest: broker("new")}}, "", nil},
|
||||
{"one of the holder's two nodes still claims it", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: broker("old")},
|
||||
{Node: "g14", Manifest: broker("new")}}, "", nil},
|
||||
{"holder's manifest unread: still assigned", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: Manifest{Module: "old"}, Unread: true}, {Node: "novox", Manifest: broker("new")}}, "", nil},
|
||||
{"an unread assignment is no claimant", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: Manifest{Module: "new"}, Unread: true}}, "", nil},
|
||||
{"no claimant keeps the record", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "novox", Manifest: dropped("other")}}, "", nil},
|
||||
{"holder gone, no claimant keeps the record", rec, nil, "", nil},
|
||||
{"one module on two nodes is two claimants: none passed, said", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: broker("new")}, {Node: "novox", Manifest: broker("new")}},
|
||||
"", []string{"old", "new on ace", "new on novox", "seat mesh-broker --to"}},
|
||||
{"two claimants, holder gone: said", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: broker("a")}, {Node: "ace", Manifest: broker("b")}},
|
||||
"", []string{"a on novox", "b on ace", "seat mesh-broker --to"}},
|
||||
{"a seat with two holders on record is not read", append(rec[:1:1], Held{Claim: "mesh-broker", Scope: ScopeMesh, Node: "ace", Module: "x"}),
|
||||
[]Assignment{{Node: "novox", Manifest: broker("new")}}, "", nil},
|
||||
{"a replicated seat is never passed",
|
||||
[]Held{{Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "novox", Module: "gone"}},
|
||||
[]Assignment{{Node: "novox", Manifest: Manifest{Module: "r", Claims: []Claim{{Name: "mesh-dns-resolver", Scope: ScopeMesh}}}}}, "", nil},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
passes, said := SeatsToPassOn(c.rec, c.as)
|
||||
if c.to == "" && len(passes) != 0 || c.to != "" && len(passes) != 1 {
|
||||
t.Fatalf("passes=%+v said=%v", passes, said)
|
||||
}
|
||||
if c.to != "" {
|
||||
p := passes[0]
|
||||
if p.Seat.Name != "mesh-broker" || p.From.Module != "old" || p.To.Node+"/"+p.To.Module != c.to {
|
||||
t.Fatalf("the pass is %+v", p)
|
||||
}
|
||||
}
|
||||
if (len(c.saidW) == 0) != (len(said) == 0) {
|
||||
t.Fatalf("said=%v, want %v", said, c.saidW)
|
||||
}
|
||||
for _, w := range c.saidW {
|
||||
if !strings.Contains(said[0], w) {
|
||||
t.Fatalf("the answer does not say %q: %s", w, said[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user