diff --git a/go/catalogue/seat_pass.go b/go/catalogue/seat_pass.go index 472eb08b..5198e816 100644 --- a/go/catalogue/seat_pass.go +++ b/go/catalogue/seat_pass.go @@ -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 { diff --git a/go/catalogue/seat_pass_test.go b/go/catalogue/seat_pass_test.go index a475228d..5abacc82 100644 --- a/go/catalogue/seat_pass_test.go +++ b/go/catalogue/seat_pass_test.go @@ -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 /"}}, {"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 /"}}, {"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) }