R-4: SeatsToPassOn resolves each assignment's build itself, with one fallback
Builds: design 47 G §199, §404, §459 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -5,14 +5,36 @@ import (
|
||||
"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).
|
||||
// An assignment as the passing of a seat reads it: a module on a node, with its placement's watched branch (design 47 G
|
||||
// §459). The build it runs is resolved by SeatsToPassOn from what the caller heard and its reader of entries.
|
||||
type Assignment struct {
|
||||
Node string
|
||||
Module string
|
||||
// Branch is the watched branch of the assignment's placement, whose newest entry of the module is taken where the node
|
||||
// said no `running` for it (§199).
|
||||
Branch string
|
||||
}
|
||||
|
||||
// Heard is what the caller heard of a module on a node from the node-engine seat's `running`: a fingerprint, or Stopped
|
||||
// (`stopped: true` and no fingerprint, §404). Nothing heard since its consumer began is no Heard at all.
|
||||
type Heard struct {
|
||||
Fingerprint string
|
||||
Stopped bool
|
||||
}
|
||||
|
||||
// EntryReader is the caller's view of the catalog half (design 47 G §459, §443): the entry's manifest by its
|
||||
// fingerprint, and a module's entry on a watched branch's newest revision. ok is false where it answers no entry.
|
||||
type EntryReader interface {
|
||||
ByFingerprint(fingerprint string) (m Manifest, ok bool)
|
||||
OnBranch(module, branch string) (m Manifest, ok bool)
|
||||
}
|
||||
|
||||
// resolved is an assignment with the manifest of the build it runs. Unread is one whose entry the reader did not answer:
|
||||
// it is still assigned, so its holdings stand, and it claims nothing.
|
||||
type resolved 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
|
||||
Unread bool
|
||||
}
|
||||
|
||||
// SeatPass is one seat whose holder on record has left it, and the only assignment that can take it.
|
||||
@@ -33,7 +55,46 @@ type SeatPass struct {
|
||||
// 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) {
|
||||
//
|
||||
// The assignments' builds are resolved here, by one rule for both callers (design 47 G §199, §459, §404): the heard
|
||||
// fingerprint's entry; with nothing heard, the module's entry on its placement's branch; a `stopped` module claims nothing
|
||||
// on that node; and where the reader answers no entry, no seat that assignment could claim passes, the holder on record
|
||||
// stays, and the answer says it, naming the node, the module and the fingerprint. The function reads no bus.
|
||||
func SeatsToPassOn(holdings []Held, assignments []Assignment, heard func(node, module string) (Heard, bool),
|
||||
entries EntryReader) ([]SeatPass, []string) {
|
||||
var rs []resolved
|
||||
var said []string
|
||||
for _, a := range assignments {
|
||||
h, ok := Heard{}, false
|
||||
if heard != nil {
|
||||
h, ok = heard(a.Node, a.Module)
|
||||
}
|
||||
if ok && h.Stopped {
|
||||
rs = append(rs, resolved{Node: a.Node, Manifest: Manifest{Module: a.Module}})
|
||||
continue
|
||||
}
|
||||
var m Manifest
|
||||
var found bool
|
||||
fp := "none heard, the newest on " + a.Branch
|
||||
if ok {
|
||||
fp = h.Fingerprint
|
||||
m, found = entries.ByFingerprint(h.Fingerprint)
|
||||
} else {
|
||||
m, found = entries.OnBranch(a.Module, a.Branch)
|
||||
}
|
||||
if !found {
|
||||
said = append(said, fmt.Sprintf("no entry is answered for %s on %s (%s): no seat it could claim passes and the holder on record stays",
|
||||
a.Module, a.Node, fp))
|
||||
rs = append(rs, resolved{Node: a.Node, Manifest: Manifest{Module: a.Module}, Unread: true})
|
||||
continue
|
||||
}
|
||||
rs = append(rs, resolved{Node: a.Node, Manifest: m})
|
||||
}
|
||||
passes, more := seatsToPass(holdings, rs)
|
||||
return passes, append(said, more...)
|
||||
}
|
||||
|
||||
func seatsToPass(holdings []Held, assignments []resolved) ([]SeatPass, []string) {
|
||||
bySeat := map[string][]Held{}
|
||||
var order []string
|
||||
for _, h := range holdings {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -27,6 +28,51 @@ func recorded(node, module string) []Held {
|
||||
return []Held{{Claim: "mesh-broker", Scope: ScopeMesh, Node: node, Module: module}}
|
||||
}
|
||||
|
||||
// asg is a case's assignment in the earlier form: a node with the manifest of the build it runs, which the harness
|
||||
// hears as a fingerprint and serves through a reader of entries; Unread is an entry the reader does not answer.
|
||||
type asg struct {
|
||||
Node string
|
||||
Manifest Manifest
|
||||
Unread bool
|
||||
// Silent is a node that said no `running`: the module's entry on its branch is taken. Stopped says it stopped.
|
||||
Silent, Stopped bool
|
||||
}
|
||||
|
||||
type entriesFixture struct{ byFP, byBranch map[string]Manifest }
|
||||
|
||||
func (e entriesFixture) ByFingerprint(fp string) (Manifest, bool) { m, ok := e.byFP[fp]; return m, ok }
|
||||
func (e entriesFixture) OnBranch(module, branch string) (Manifest, bool) {
|
||||
m, ok := e.byBranch[module+"@"+branch]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
func passOn(rec []Held, as []asg) ([]SeatPass, []string) {
|
||||
e := entriesFixture{map[string]Manifest{}, map[string]Manifest{}}
|
||||
var assignments []Assignment
|
||||
heard := map[string]Heard{}
|
||||
for i, a := range as {
|
||||
fp := fmt.Sprintf("fp%d", i)
|
||||
assignments = append(assignments, Assignment{Node: a.Node, Module: a.Manifest.Module, Branch: "trunk"})
|
||||
switch {
|
||||
case a.Stopped:
|
||||
heard[a.Node+"/"+a.Manifest.Module] = Heard{Stopped: true}
|
||||
case a.Silent:
|
||||
if !a.Unread {
|
||||
e.byBranch[a.Manifest.Module+"@trunk"] = a.Manifest
|
||||
}
|
||||
default:
|
||||
heard[a.Node+"/"+a.Manifest.Module] = Heard{Fingerprint: fp}
|
||||
if !a.Unread {
|
||||
e.byFP[fp] = a.Manifest
|
||||
}
|
||||
}
|
||||
}
|
||||
return SeatsToPassOn(rec, assignments, func(node, module string) (Heard, bool) {
|
||||
h, ok := heard[node+"/"+module]
|
||||
return h, ok
|
||||
}, e)
|
||||
}
|
||||
|
||||
// novox/hq ADR 0660 "How it is checked", and ADR 0449 §4's original case.
|
||||
func TestSeatsToPassOn(t *testing.T) {
|
||||
passSeat(t)
|
||||
@@ -34,46 +80,57 @@ func TestSeatsToPassOn(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
rec []Held
|
||||
as []Assignment
|
||||
as []asg
|
||||
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},
|
||||
[]asg{{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},
|
||||
[]asg{{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}}}},
|
||||
[]asg{{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},
|
||||
[]asg{{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")},
|
||||
[]asg{{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},
|
||||
[]asg{{Node: "novox", Manifest: Manifest{Module: "old"}, Unread: true}, {Node: "novox", Manifest: broker("new")}}, "", []string{"no entry is answered for old on novox"}},
|
||||
{"an unread assignment is no claimant", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: Manifest{Module: "new"}, Unread: true}}, "", nil},
|
||||
[]asg{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: Manifest{Module: "new"}, Unread: true}}, "", []string{"no entry is answered for new on ace"}},
|
||||
{"a claimant whose running manifest does not claim is not counted", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: dropped("new")}}, "", nil},
|
||||
[]asg{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: dropped("new")}}, "", nil},
|
||||
{"no claimant keeps the record", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "novox", Manifest: dropped("other")}}, "", nil},
|
||||
[]asg{{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")}},
|
||||
[]asg{{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 <node>/<module>"}},
|
||||
{"two claimants, holder gone: said", rec,
|
||||
[]Assignment{{Node: "novox", Manifest: broker("a")}, {Node: "ace", Manifest: broker("b")}},
|
||||
[]asg{{Node: "novox", Manifest: broker("a")}, {Node: "ace", Manifest: broker("b")}},
|
||||
"", []string{"a on novox", "b on ace", "seat mesh-broker --to <node>/<module>"}},
|
||||
{"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},
|
||||
[]asg{{Node: "novox", Manifest: broker("new")}}, "", nil},
|
||||
// Design 47 G §459, §199, §404.
|
||||
{"nothing heard: the module's entry on its branch is taken", rec,
|
||||
[]asg{{Node: "novox", Manifest: dropped("old"), Silent: true}, {Node: "ace", Manifest: broker("new"), Silent: true}}, "ace/new", nil},
|
||||
{"nothing heard of the holder: its entry on the branch still claims, so it keeps the seat", rec,
|
||||
[]asg{{Node: "novox", Manifest: broker("old"), Silent: true}, {Node: "ace", Manifest: broker("new")}}, "", nil},
|
||||
{"a stopped holder passes no seat to itself: the one claimant takes it", rec,
|
||||
[]asg{{Node: "novox", Manifest: broker("old"), Stopped: true}, {Node: "ace", Manifest: broker("new")}}, "ace/new", nil},
|
||||
{"a stopped module claims nothing", rec,
|
||||
[]asg{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: broker("new"), Stopped: true}}, "", nil},
|
||||
{"an entry not answered keeps the holder on record and says it", rec,
|
||||
[]asg{{Node: "novox", Manifest: dropped("old"), Unread: true}, {Node: "ace", Manifest: broker("new")}}, "", []string{"no entry is answered for old on novox (fp0)"}},
|
||||
{"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},
|
||||
[]asg{{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)
|
||||
passes, said := passOn(c.rec, c.as)
|
||||
if c.to == "" && len(passes) != 0 || c.to != "" && len(passes) != 1 {
|
||||
t.Fatalf("passes=%+v said=%v", passes, said)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user