Files
mesh-delivery/modules/mesh-delivery-view/internal/view/tiers.go
T
jschoubben 06e5980898 VW2: the view's waits are the parts' wait-held, kept in its state waits
The view's process hears wait-held and wait-ended of every part (seats and modules; waits.go, manifest consumes) and keeps them in
its state waits under owner.step.node.module (viewstate/waits.go); view_tiers reads them through the tools bundle and names each
module's waits in the sayer's own words, {owner, step, node, on, bound, since, words} (tiers.go waitsHeld), beside the indexer's
pending revisions as the seat mesh-catalog's step take-in. No state of another module is read for a tier's waits.

Builds: design 47 G §512, §376, §498, A-242
2026-11-12 09:19:18 +01:00

289 lines
11 KiB
Go

package view
import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
// The tiers of a source revision are a view, worked out at each call and never stored (novox/hq ADR 0603 §1, §6): the
// modules the revision moves, ordered by the module graph's ordering edges among them (a base, a provision or bus state
// awaited from another of them, a verb another of them serves whose edge the graph keeps, ADR 0614 §1), and for each module
// what it waits on now: its build key, each node's want (named by its edge: a verb, a reader capability) and have, and
// each provision it awaits on each node, with the rule and the words of the part that holds it, and the verb edges the
// graph leaves out. Nothing in the mesh waits on
// a tier; the tiers say where the revision stands. A node the watchman says is offline is named away and left out of
// what the revision waits on (ADR 0603 §7).
// TiersTool is the view's read tool of a source revision's tiers.
const TiersTool = "view_tiers"
// TiersMost is the most modules view_tiers answers (ADR 0603 §2); past it the answer says how many it left out.
const TiersMost = 500
// Tiers is a source revision's tiers: Revision the source revision (the gate's source key's revision), Modules how many
// modules it moves (each whose fingerprint last changed at it, meshcatalog.ModulesByModuleValue.FPSrc), Tiers those modules in order, the
// first tier waiting on none of the others. Cut says when the answer holds fewer than Modules, Graph why the module graph
// could not be read (then every module is in the first tier), Cycle a cycle among them (its modules form the last
// tier), and Away the nodes left out.
type Tiers struct {
// Branch is the watched branch whose newest source revision it is, when view_tiers was given no revision (A-173).
Branch string `json:"branch,omitempty"`
Revision uint64 `json:"revision"`
Modules int `json:"modules"`
Tiers [][]TierModule `json:"tiers"`
Cut string `json:"cut,omitempty"`
Graph string `json:"graph,omitempty"`
// Pending says why the indexer's waits could not be read, so no module is named held by its revision's indexer (ADR 0633 §5).
Pending string `json:"pending,omitempty"`
}
// TierModule is one module of a tier (design 47 G §512): its build (the fingerprint the difference says it is to run), the
// modules it is ordered after with the edge's kind, and each wait of its own now, from the parts' wait-held and the indexer's
// pending revisions (no wait is a module that reached every node present).
type TierModule struct {
Module string `json:"module"`
// FP is the build.
FP string `json:"build,omitempty"`
After []TierAfter `json:"after,omitempty"`
Waits []TierWait `json:"waits,omitempty"`
}
// TierAfter is a module a module is ordered after, with the kind of the edge that orders it (design 47 G §512, §378).
type TierAfter struct {
Module string `json:"module"`
Kind string `json:"kind"`
}
// TierWait is one wait of a module, as the part that holds it said it (design 47 G §512, §498): the seat or module that holds it
// (Owner), its step, the node it is on, what it waits on, its bound, since when, and its own words. The pending revision of the
// indexer is Owner mesh-catalog and Step take-in.
type TierWait struct {
Owner string `json:"owner"`
Step string `json:"step"`
Node string `json:"node,omitempty"`
On string `json:"on,omitempty"`
Bound string `json:"bound,omitempty"`
Since time.Time `json:"since,omitzero"`
Words string `json:"words,omitempty"`
}
// BranchTiers is view_tiers' answer given no revision: the tiers of each watched branch's newest source revision, in branch
// order (design 47 G A-173).
type BranchTiers struct {
Branches []Tiers `json:"branches"`
}
// Newest works out the tiers of each watched branch's newest source revision, as the gate's verb source answers it.
func (r Reader) Newest() (BranchTiers, error) {
if r.Sources == nil {
return BranchTiers{}, fmt.Errorf("the gate's source cannot be read here: name the source revision")
}
revs, err := r.Sources()
if err != nil {
return BranchTiers{}, fmt.Errorf("the gate's source: %w", err)
}
out := BranchTiers{Branches: []Tiers{}}
for _, branch := range slices.Sorted(maps.Keys(revs)) {
if revs[branch] == 0 {
continue
}
t, err := r.Tiers(revs[branch])
if err != nil {
return BranchTiers{}, fmt.Errorf("%s: %w", branch, err)
}
t.Branch = branch
out.Branches = append(out.Branches, t)
}
if len(out.Branches) == 0 {
return BranchTiers{}, fmt.Errorf("the gate holds no source revision of any watched branch yet")
}
return out, nil
}
// Tiers works out the tiers of one source revision (not 0): the difference of the kept assignment revision that carries it
// against the one kept before it, as the assigner's verb difference answers it with its tiers and each change's after
// (design 47 G §512, §186, A-242). The view orders nothing itself.
func (r Reader) Tiers(revision uint64) (Tiers, error) {
if revision == 0 {
return Tiers{}, fmt.Errorf("name the source revision")
}
out := Tiers{Revision: revision}
from, to, err := r.revisionsOf(revision)
if err != nil {
return Tiers{}, err
}
var d meshwants.Difference
if from > 0 {
if d, err = r.Difference(from, to); err != nil {
return Tiers{}, fmt.Errorf("the assigner's difference of %d and %d: %w", from, to, err)
}
} else {
out.Graph = fmt.Sprintf("no assignment revision is kept before %d, so the difference names nothing", to)
}
// One module per name, in the highest tier any of its nodes is in; the changes name each module's build and what it is
// ordered after.
changes := map[string][]meshwants.Change{}
for _, c := range d.Changes {
changes[c.Module] = append(changes[c.Module], c)
}
tierOf := map[string]int{}
for i, tier := range d.Tiers {
for _, ref := range tier {
tierOf[ref.Module] = max(tierOf[ref.Module], i)
}
}
names := slices.Sorted(maps.Keys(changes))
out.Modules = len(names)
if len(names) > TiersMost {
out.Cut = fmt.Sprintf("the revision moves %d modules: the first %d by name are shown, %s and the %d after it are not",
len(names), TiersMost, names[TiersMost], len(names)-TiersMost-1)
names = names[:TiersMost]
}
waits := r.waitsHeld(&out)
held, heldAll := r.heldBack(&out)
in := map[string]bool{}
for _, n := range names {
in[n] = true
}
for _, n := range names {
t := tierOf[n]
for len(out.Tiers) <= t {
out.Tiers = append(out.Tiers, nil)
}
tm := TierModule{Module: n}
seen := map[string]bool{}
for _, c := range changes[n] {
if tm.FP == "" {
tm.FP = c.To
}
for _, a := range c.After {
if k := a.Module + "\x00" + a.Kind; !seen[k] && a.Module != n {
seen[k] = true
tm.After = append(tm.After, TierAfter{Module: a.Module, Kind: a.Kind})
}
}
}
slices.SortFunc(tm.After, func(a, b TierAfter) int { return strings.Compare(a.Module+"\x00"+a.Kind, b.Module+"\x00"+b.Kind) })
tm.Waits = waits[n]
if w, ok := held[n]; ok {
tm.Waits = append(tm.Waits, w)
}
out.Tiers[t] = append(out.Tiers[t], tm)
}
// A module the indexer holds back is not at the revision yet, so no tier of it names it: it is named in the first tier with
// its wait alone (ADR 0633 §5).
for _, n := range heldAll {
if in[n] {
continue
}
if len(out.Tiers) == 0 {
out.Tiers = append(out.Tiers, nil)
}
out.Tiers[0] = append(out.Tiers[0], TierModule{Module: n, Waits: []TierWait{held[n]}})
}
compact := out.Tiers[:0]
for _, t := range out.Tiers {
if len(t) > 0 {
compact = append(compact, t)
}
}
out.Tiers = compact
return out, nil
}
// TakeInStep and TakeInOwner are the step and the owner of a wait on the indexer a source revision or a group is held for
// (ADR 0633 §5, design 47 G §512): the pending revision's wait is the seat mesh-catalog's step take-in.
const (
TakeInStep = "take-in"
TakeInOwner = "mesh-catalog"
)
// heldBack are the modules the indexer holds back, by module, each with its wait, read through the indexer's tool
// indexer_pending (ADR 0633 §5), and the same modules sorted. A tool that cannot be asked says so in out.Pending.
func (r Reader) heldBack(out *Tiers) (map[string]TierWait, []string) {
held := map[string]TierWait{}
if r.Pending == nil {
return held, nil
}
a, err := r.Pending()
if err != nil {
out.Pending = "the indexer's pending revisions cannot be read, so no module is named waiting for its revision's indexer: " + err.Error()
return held, nil
}
for _, w := range a.Waits {
words := "the indexer holds back " + w.Key + ", which waits for the indexer build " + w.WaitsFor
if w.What == takein.TakeInGroup {
words = "the indexer gathers the group " + w.Group + " and its end has not come"
} else if w.Behind != "" {
words = "the indexer holds back " + w.Key + " behind " + w.Behind
}
for _, module := range w.Holds {
if _, ok := held[module]; !ok { // the oldest wait names it
held[module] = TierWait{Owner: TakeInOwner, Step: TakeInStep, Words: words, Since: w.Since}
}
}
}
return held, slices.Sorted(maps.Keys(held))
}
// waitsHeld are the waits the parts hold, by module, from the view's own state waits (written from their wait-held and wait-ended,
// design 47 G §512, §376, A-242): each in the words its sayer gave. A state that cannot be read says so in out.Pending.
func (r Reader) waitsHeld(out *Tiers) map[string][]TierWait {
held := map[string][]TierWait{}
if r.WaitsState.Keys == nil || r.WaitsState.Get == nil {
return held
}
all, err := values(r.WaitsState)
if err != nil {
out.Pending += "the parts' waits cannot be read, so no module is named waiting on a part: " + err.Error()
return held
}
for _, key := range slices.Sorted(maps.Keys(all)) {
w, err := viewstate.ReadWait("", all[key])
if err != nil || w.Module == "" {
continue
}
held[w.Module] = append(held[w.Module], TierWait{Owner: w.Owner, Step: w.Step, Node: w.Node, On: w.On, Bound: w.Bound, Since: w.Since, Words: w.Words})
}
return held
}
// tiersRevision reads the tool's revision argument: a number, as JSON gives it or as text; 0 for the gate's newest.
func tiersRevision(v any) (uint64, error) {
switch x := v.(type) {
case nil:
return 0, nil
case float64:
if x < 0 || x != float64(uint64(x)) {
return 0, fmt.Errorf("the revision %v is no source revision", x)
}
return uint64(x), nil
case string:
if strings.TrimSpace(x) == "" {
return 0, nil
}
return strconv.ParseUint(strings.TrimSpace(x), 10, 64)
}
return 0, fmt.Errorf("the revision %v is no source revision", v)
}
// TiersOf is view_tiers' answer for its arguments.
func (r Reader) TiersOf(args map[string]any) (any, error) {
revision, err := tiersRevision(args["revision"])
if err != nil {
return nil, err
}
if revision == 0 {
return r.Newest()
}
return r.Tiers(revision)
}