Files
mesh-delivery/modules/mesh-delivery-view/internal/view/tiers.go
T

386 lines
12 KiB
Go

package view
import (
"encoding/json"
"fmt"
"maps"
"slices"
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
// 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 awaited from
// another of them), and for each module what it waits on now: its build key, each node's want and have, and each
// provision it awaits on each node, with the rule and the words of the part that holds it. 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, delivery.Module.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 {
Revision uint64 `json:"revision"`
Modules int `json:"modules"`
Tiers [][]TierModule `json:"tiers"`
Cut string `json:"cut,omitempty"`
Graph string `json:"graph,omitempty"`
Cycle []string `json:"cycle,omitempty"`
Away []string `json:"away,omitempty"`
}
// TierModule is one module of a tier: its fingerprint, the modules of the same revision it waits for (After), and each
// wait of its own now; no wait is a module that reached every node present.
type TierModule struct {
Module string `json:"module"`
FP string `json:"fp,omitempty"`
None string `json:"none,omitempty"`
After []string `json:"after,omitempty"`
Waits []TierWait `json:"waits,omitempty"`
}
// TierWait is one thing a module waits on: the edge (build, want, have, or provision <name>), the node it waits on (none
// for its build), the assigner's rule for a want held back, the part's words and since when, where the part says it.
type TierWait struct {
Edge string `json:"edge"`
Node string `json:"node,omitempty"`
Rule int `json:"rule,omitempty"`
Words string `json:"words"`
Since time.Time `json:"since,omitzero"`
}
// Tiers works out the tiers of a source revision, the gate's newest when revision is 0.
func (r Reader) Tiers(revision uint64) (Tiers, error) {
if revision == 0 {
if r.SourceState.GetEntry == nil {
return Tiers{}, fmt.Errorf("the gate's source cannot be read here: name the source revision")
}
_, rev, err := r.SourceState.GetEntry(delivery.SourceMainKey)
if err != nil {
return Tiers{}, fmt.Errorf("the gate's source: %w", err)
}
if rev == 0 {
return Tiers{}, fmt.Errorf("the gate holds no source revision yet")
}
revision = rev
}
modules, err := values(r.State)
if err != nil {
return Tiers{}, fmt.Errorf("the indexer's modules: %w", err)
}
moved := map[string]delivery.Module{}
for key, raw := range modules {
if m, err := delivery.DecodeModule(raw); err == nil && m.FPSrc == revision {
moved[key] = m
}
}
out := Tiers{Revision: revision, Modules: len(moved)}
names := slices.Sorted(maps.Keys(moved))
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]
}
gr := graph.Graph{}
if raws, err := values(r.GraphState); err != nil {
out.Graph = "the module graph cannot be read, so no module is ordered after another: " + err.Error()
} else {
for key, raw := range raws {
if n, err := graph.Decode(raw); err == nil {
gr[key] = n
}
}
}
away := r.away()
out.Away = slices.Sorted(maps.Keys(away))
wants, _ := values(r.WantsState)
provided, perr := values(r.ProvisionsState)
// The ordering among the revision's modules: each one's tier is one past the highest of the modules it waits for.
in := map[string]bool{}
for _, n := range names {
in[n] = true
}
after := map[string][]string{}
for _, n := range names {
for _, b := range gr.Before(n) {
if in[b] {
after[n] = append(after[n], b)
}
}
}
sub := graph.Graph{}
for _, n := range names {
if g, ok := gr[n]; ok {
sub[n] = g
}
}
out.Cycle = sub.Cycle()
tier := map[string]int{}
var level func(string, map[string]bool) int
level = func(n string, visiting map[string]bool) int {
if t, ok := tier[n]; ok {
return t
}
if visiting[n] {
return -1
}
visiting[n] = true
t := 0
for _, b := range after[n] {
lb := level(b, visiting)
if lb < 0 {
return -1
}
t = max(t, lb+1)
}
delete(visiting, n)
tier[n] = t
return t
}
top := 0
for _, n := range names {
if t := level(n, map[string]bool{}); t > top {
top = t
}
}
for _, n := range names {
if _, ok := tier[n]; !ok {
tier[n] = top + 1 // on a cycle: after every tier that is ordered
}
}
for _, n := range names {
t := tier[n]
for len(out.Tiers) <= t {
out.Tiers = append(out.Tiers, nil)
}
m := moved[n]
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n]}
tm.Waits = r.waitsOf(n, m, gr[n], wants, provided, perr, away)
out.Tiers[t] = append(out.Tiers[t], tm)
}
compact := out.Tiers[:0]
for _, t := range out.Tiers {
if len(t) > 0 {
compact = append(compact, t)
}
}
out.Tiers = compact
return out, nil
}
// away are the nodes the watchman's presence says are offline.
func (r Reader) away() map[string]bool {
out := map[string]bool{}
presence, err := values(r.PresenceState)
if err != nil {
return out
}
for key, raw := range presence {
node, part, _, err := delivery.ParsePresenceKey(key)
if err != nil || part != "" {
continue
}
if p, err := delivery.DecodePresence(raw); err == nil && !p.Online {
out[node] = true
}
}
return out
}
// waitsOf is what module m waits on now: its build at its fingerprint, then on each node present that wants it, its want,
// its have, and each provision its graph node awaits.
func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, provided map[string]json.RawMessage,
perr error, away map[string]bool) []TierWait {
var out []TierWait
if m.FP2 == "" {
return []TierWait{{Edge: "build", Words: "it has no fingerprint, so nothing is built: " + m.None2}}
}
if w, ok := r.buildWait(module, m.FP2); ok {
out = append(out, w)
}
var nodes []string
for key := range wants {
node, mod, err := delivery.ParseWantKey(key)
if err == nil && mod == module {
nodes = append(nodes, node)
}
}
slices.Sort(nodes)
for _, node := range nodes {
if away[node] {
continue
}
w, err := delivery.DecodeWant(wants[mustWantKey(node, module)])
if err != nil || w.Removed {
continue
}
if w.FP != m.FP2 {
words := w.Words
if words == "" {
words = "the want names " + short(w.FP) + ", not " + short(m.FP2)
}
out = append(out, TierWait{Edge: "want", Node: node, Rule: w.Rule, Words: words, Since: r.since(r.WantsState, mustWantKey(node, module))})
} else if hw, ok := r.haveWait(node, module, m.FP2); ok {
out = append(out, hw)
}
for _, a := range g.Awaits {
out = append(out, provisionWaits(node, module, a, provided, perr)...)
}
}
return out
}
func mustWantKey(node, module string) string {
k, _ := delivery.WantKey(node, module)
return k
}
// buildWait is the wait of the build key <module>.<fp>, none when it is done.
func (r Reader) buildWait(module, fp string) (TierWait, bool) {
key, err := delivery.BuilderBuildKey(module, fp)
if err != nil || r.Builds.Get == nil {
return TierWait{Edge: "build", Words: "the builder's builds cannot be read here"}, true
}
raw, err := r.Builds.Get(key)
switch {
case err != nil:
return TierWait{Edge: "build", Words: "the build key " + key + " cannot be read: " + err.Error()}, true
case raw == nil:
return TierWait{Edge: "build", Words: "the builder holds no build of " + short(fp) + " yet"}, true
}
b, err := delivery.DecodeBuilderBuild(raw)
if err != nil {
return TierWait{Edge: "build", Words: "the build key " + key + " does not read: " + err.Error()}, true
}
switch b.State {
case delivery.BuildDone:
return TierWait{}, false
case delivery.BuildFailed:
return TierWait{Edge: "build", Words: "the build of " + short(fp) + " failed: " + b.Error}, true
}
words := "the build of " + short(fp) + " is " + b.State
if b.Waits != "" {
words += ", waiting for the package " + b.Waits
}
if b.Step != "" {
words += ": " + b.Step
}
return TierWait{Edge: "build", Words: words, Since: r.since(r.Builds, key)}, true
}
// haveWait is the wait of node's have of module at fp, none when it runs fp healthy.
func (r Reader) haveWait(node, module, fp string) (TierWait, bool) {
key, err := delivery.HaveKey(node, module)
if err != nil || r.HavesState.Get == nil {
return TierWait{}, false
}
raw, err := r.HavesState.Get(key)
if err != nil || raw == nil {
return TierWait{Edge: "have", Node: node, Words: node + " has not yet said what it runs of " + module}, true
}
h, err := delivery.DecodeHave(raw)
if err != nil {
return TierWait{Edge: "have", Node: node, Words: "its have does not read: " + err.Error()}, true
}
if h.FP == fp && h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK {
return TierWait{}, false
}
words := fmt.Sprintf("%s runs %s, %s", node, short(h.FP), h.Phase)
if h.Health != "" {
words += ", health " + string(h.Health)
}
if h.Detail != "" {
words += ": " + h.Detail
}
return TierWait{Edge: "have", Node: node, Words: words, Since: r.since(r.HavesState, key)}, true
}
// provisionWaits are the waits of the provision a that module awaits on node: none once every key of it is provided.
func provisionWaits(node, module, a string, provided map[string]json.RawMessage, perr error) []TierWait {
edge := "provision " + a
if perr != nil {
return []TierWait{{Edge: edge, Node: node, Words: "the provisioner's keys cannot be read: " + perr.Error()}}
}
prefix, _ := provisions.Key(node, module, a, "")
var out []TierWait
found := false
for _, key := range slices.Sorted(maps.Keys(provided)) {
if key != prefix && !strings.HasPrefix(key, prefix+".") {
continue
}
found = true
p, err := provisions.Decode(provided[key])
if err != nil {
out = append(out, TierWait{Edge: edge, Node: node, Words: "the provision key " + key + " does not read: " + err.Error()})
continue
}
if p.State == provisions.Provided {
continue
}
words := "the provision " + key + " is " + p.State
if p.Why != "" {
words += ": " + p.Why
}
out = append(out, TierWait{Edge: edge, Node: node, Words: words, Since: p.At})
}
if !found {
out = append(out, TierWait{Edge: edge, Node: node, Words: "the provisioner holds no key of " + a + " for " + module + " on " + node + " yet"})
}
return out
}
// since is when key's value was written, from the state's history, or zero when it cannot say.
func (r Reader) since(s StateRead, key string) time.Time {
if s.History == nil {
return time.Time{}
}
revs, err := s.History(key)
if err != nil || len(revs) == 0 {
return time.Time{}
}
return revs[len(revs)-1].At
}
// 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
}
return r.Tiers(revision)
}