novox/hq 04-ISSUES/125. A module assigned to a machine and never taken runs none of what it declares. Status had no vocabulary for it: the machine was heard from, current, and doing what it was told, so the mesh printed "all doing what they were told" — which was true, and was acted on, and every public name on the machine went dark. Status now names each module a machine is holding rather than running, per machine and with a count, read from what the MACHINE reported rather than from the mesh's take-time listing — the machine is the only thing that knows what it found. The JSON form carries the same rows, absent rather than empty when nothing is held. And a hold suppresses the all-well sentence, where being adopted does not: adopted is a mode somebody chose, a module assigned and never taken is a half-finished action with nothing left to finish it. The condition is now a named function so the rule lives in one place and a test binds to the real thing rather than a copy of it. untakenModules raises a read it cannot make rather than answering "holding nothing" from a failed query, which is the shape this whole issue is.
229 lines
9.4 KiB
Go
229 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// The same answers, in a shape something other than a person can read.
|
|
//
|
|
// A board reads through interfaces and holds nothing (novox/hq 03-DESIGN/01-to-be/11-a-board.md).
|
|
// Everything it needs is already answered by these commands — as text, for people, which is not
|
|
// something a page can read. So each of them can say it again as JSON.
|
|
//
|
|
// **`--json` rather than a serving API**, because nothing needs one yet: whatever serves a board
|
|
// runs the command, and the constraint in the design holds either way — the board never touches a
|
|
// context's store. An API is the larger thing and should wait until something is asking for it.
|
|
//
|
|
// **These shapes are hard to change once anything is built against them.** So they stay close to
|
|
// what the domain already calls things, and carry no summary field that would have to be kept
|
|
// true. Nothing here is derived that a reader could not derive.
|
|
|
|
// meshStatus is what `status --json` says: the three questions, in the order they are asked.
|
|
type meshStatus struct {
|
|
// Wrong is every machine whose last declaration was refused or partly failed.
|
|
Wrong []machineDoing `json:"wrong"`
|
|
// Quiet is every machine not heard from lately. Not the same as wrong: new, switched off and
|
|
// unreachable are not "tried and could not".
|
|
Quiet []machineQuiet `json:"quiet"`
|
|
// Behind is every module built from something older than its source has.
|
|
Behind []moduleBehind `json:"behind"`
|
|
// Waiting is every machine not running what the mesh would send it. The same question as
|
|
// Behind one level down: that says the catalogue is old, this says a machine is — and only
|
|
// this one has somebody's change waiting inside it.
|
|
Waiting []machineWaiting `json:"waiting"`
|
|
// Reported is every machine's last word beside when it was last sent a declaration. A
|
|
// machine whose report is newer than its send has acted on the current declaration; one
|
|
// whose is older is still working — and Waiting cannot tell those apart, because the sent
|
|
// digest is recorded at send, not at apply.
|
|
Reported []machineReported `json:"reported"`
|
|
// Unresolved is every machine that cannot be worked out at all, with what the mesh said when
|
|
// it tried. **A machine here is in none of the lists above**: nothing was computed for it, so
|
|
// there is nothing to compare it against and nothing it can be behind — which is why a
|
|
// document without this field described a wholly blocked mesh as a well one.
|
|
//
|
|
// Per machine, and data. One node failing must never take the document away from a reader
|
|
// asking about the others.
|
|
Unresolved []machineUnresolved `json:"unresolved"`
|
|
// Network is why the private network could not be computed, when it could not; absent when it
|
|
// could. Almost always a consequence of Unresolved: a node that does not resolve is not on the
|
|
// network, and a mesh whose hub is that node has no hub.
|
|
Network string `json:"network,omitempty"`
|
|
// Machines is how many the mesh knows about, so a reader can tell "none wrong" from
|
|
// "none at all".
|
|
Machines int `json:"machines"`
|
|
// Adopted is every node still adopted (novox/hq ADR 0100); absent when none is.
|
|
Adopted []string `json:"adopted,omitempty"`
|
|
// Untaken is every module assigned to a machine that is holding what it found rather than
|
|
// running what the module declares, because nothing took it (novox/hq 04-ISSUES/125). Absent
|
|
// when nothing is held.
|
|
//
|
|
// **A document without this said an outage was a well mesh.** Read from what each machine
|
|
// reported, so it is the machine's account and not the mesh's take-time listing.
|
|
Untaken []machineUntaken `json:"untaken,omitempty"`
|
|
}
|
|
|
|
// machineUntaken is one module a machine is holding rather than running, and how many resources of
|
|
// it are held.
|
|
type machineUntaken struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
// Held is how many of the module's resources the machine is keeping as it found them. Zero is
|
|
// impossible here: a module with nothing held is not in this list.
|
|
Held int `json:"held"`
|
|
}
|
|
|
|
type machineUnresolved struct {
|
|
Node string `json:"node"`
|
|
// Problem is the mesh's own words, whole — newlines and all. It lists every requirement that
|
|
// could not be met, and a first line alone would name one of them and hide the rest.
|
|
Problem string `json:"problem"`
|
|
}
|
|
|
|
type machineDoing struct {
|
|
Node string `json:"node"`
|
|
// Outcome is refused or failed. Kept distinct all the way out: they are fixed in different
|
|
// places, and one word for both sends half the readers to the wrong one.
|
|
Outcome string `json:"outcome"`
|
|
Refused string `json:"refused,omitempty"`
|
|
Failed []struct {
|
|
ID string `json:"id"`
|
|
Error string `json:"error"`
|
|
} `json:"failed,omitempty"`
|
|
Applied int `json:"applied"`
|
|
At time.Time `json:"at"`
|
|
// Since is when this same failure was first reported and Times how many reports in a row
|
|
// have said it; Stuck is the mesh's word for "enough of them" (novox/hq 04-ISSUES/065).
|
|
Since *time.Time `json:"since,omitempty"`
|
|
Times int `json:"times"`
|
|
Stuck bool `json:"stuck"`
|
|
}
|
|
|
|
type machineReported struct {
|
|
Node string `json:"node"`
|
|
Outcome string `json:"outcome"`
|
|
At *time.Time `json:"at,omitempty"`
|
|
Sent *time.Time `json:"sent,omitempty"`
|
|
// Current is whether the last report names the declaration last sent. Not derivable from
|
|
// the timestamps beside it: an apply begun under the previous declaration reports after the
|
|
// next send, newer and still about the old words.
|
|
Current bool `json:"current"`
|
|
}
|
|
|
|
type machineWaiting struct {
|
|
Node string `json:"node"`
|
|
// Never is true when nothing has ever been sent to it. Not out of date: nobody has ever asked
|
|
// this machine to be anything, and the two read differently to whoever is looking.
|
|
Never bool `json:"never"`
|
|
Sent *time.Time `json:"sent,omitempty"`
|
|
}
|
|
|
|
type machineQuiet struct {
|
|
Node string `json:"node"`
|
|
// LastSeen is absent when the machine has never spoken, which is a different thing from
|
|
// having been quiet for a while.
|
|
LastSeen *time.Time `json:"lastSeen,omitempty"`
|
|
}
|
|
|
|
type moduleBehind struct {
|
|
Module string `json:"module"`
|
|
BuiltFrom string `json:"builtFrom"`
|
|
Head string `json:"head"`
|
|
On []string `json:"on"`
|
|
}
|
|
|
|
// statusAsJSON answers the same questions as the text form, from the same reading.
|
|
//
|
|
// **It takes the whole reading rather than a growing argument list**, which is what let a new
|
|
// answer be added to the text form and forgotten here — the two are one function's output in two
|
|
// shapes, and they must not be able to differ about what was asked.
|
|
//
|
|
// It never fails on account of the mesh. Every per-machine problem in here is a field, so one
|
|
// machine that cannot be worked out cannot stop a caller reading about the others: a
|
|
// machine-readable interface that stops being machine-readable exactly when something is wrong is
|
|
// one nobody can build an alarm on.
|
|
func statusAsJSON(asked answers) ([]byte, error) {
|
|
wrong, nodes, quiet := asked.wrong, asked.nodes, asked.quiet
|
|
behind, sources := asked.behind, asked.sources
|
|
waiting, reported := asked.waiting, asked.reported
|
|
|
|
out := meshStatus{Machines: len(nodes), Wrong: []machineDoing{},
|
|
Quiet: []machineQuiet{}, Behind: []moduleBehind{}, Waiting: []machineWaiting{},
|
|
Reported: []machineReported{}, Unresolved: []machineUnresolved{},
|
|
Network: asked.network, Adopted: adoptedNodes(nodes)}
|
|
// In a stated order, so two readings of an unchanged mesh are the same document.
|
|
untakenNodes := make([]string, 0, len(asked.untaken))
|
|
for name := range asked.untaken {
|
|
untakenNodes = append(untakenNodes, name)
|
|
}
|
|
sort.Strings(untakenNodes)
|
|
for _, name := range untakenNodes {
|
|
modules := make([]string, 0, len(asked.untaken[name]))
|
|
for m := range asked.untaken[name] {
|
|
modules = append(modules, m)
|
|
}
|
|
sort.Strings(modules)
|
|
for _, m := range modules {
|
|
out.Untaken = append(out.Untaken,
|
|
machineUntaken{Node: name, Module: m, Held: asked.untaken[name][m]})
|
|
}
|
|
}
|
|
for name := range asked.refused {
|
|
out.Unresolved = append(out.Unresolved, machineUnresolved{
|
|
Node: name, Problem: asked.refused[name]})
|
|
}
|
|
sort.Slice(out.Unresolved, func(i, j int) bool {
|
|
return out.Unresolved[i].Node < out.Unresolved[j].Node
|
|
})
|
|
for _, r := range reported {
|
|
out.Reported = append(out.Reported, machineReported{
|
|
Node: r.Node, Outcome: r.Outcome, At: r.At, Sent: r.Sent, Current: r.Current})
|
|
}
|
|
|
|
for _, m := range waiting {
|
|
out.Waiting = append(out.Waiting, machineWaiting{
|
|
Node: m.Node, Never: m.Never, Sent: m.SentAt})
|
|
}
|
|
|
|
for _, d := range wrong {
|
|
row := machineDoing{
|
|
Node: d.Node, Outcome: d.Outcome, Refused: d.Refused, Applied: d.Applied, At: d.At,
|
|
Since: d.Since, Times: d.Times, Stuck: d.Stuck(),
|
|
}
|
|
for _, f := range d.Failed {
|
|
row.Failed = append(row.Failed, struct {
|
|
ID string `json:"id"`
|
|
Error string `json:"error"`
|
|
}{ID: f.ID, Error: f.Error})
|
|
}
|
|
out.Wrong = append(out.Wrong, row)
|
|
}
|
|
for _, n := range quiet {
|
|
row := machineQuiet{Node: n.Name}
|
|
if !n.LastSeen.IsZero() {
|
|
seen := n.LastSeen
|
|
row.LastSeen = &seen
|
|
}
|
|
out.Quiet = append(out.Quiet, row)
|
|
}
|
|
for module, on := range behind {
|
|
from := sources[module]
|
|
out.Behind = append(out.Behind, moduleBehind{
|
|
Module: module, BuiltFrom: from.BuiltFrom, Head: from.Head, On: on,
|
|
})
|
|
}
|
|
return json.MarshalIndent(out, "", " ")
|
|
}
|
|
|
|
// say prints a value as JSON, for the commands that can answer either way.
|
|
func say(value any) error {
|
|
body, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(body))
|
|
return nil
|
|
}
|