384 lines
12 KiB
Go
384 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-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]
|
|
}
|
|
graph := delivery.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 := delivery.DecodeGraphNode(raw); err == nil {
|
|
graph[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 graph.Before(n) {
|
|
if in[b] {
|
|
after[n] = append(after[n], b)
|
|
}
|
|
}
|
|
}
|
|
sub := delivery.Graph{}
|
|
for _, n := range names {
|
|
if g, ok := graph[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, graph[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 delivery.GraphNode, 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)
|
|
}
|