mesh-self-check writes its findings through mesh-sdk's finding writer and the generated finding
Builds: design 47 G §686, §687, §689, A-3, A-13, A-14, A-16 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
// The self-check's findings are the generated finding of the seat mesh-conditions, written through mesh-sdk's finding
|
||||
// writer (design 47 G §461, §689); the probes build them with these two helpers.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// Finding, and the writer's other three bodies, are the seat mesh-conditions' own types.
|
||||
type (
|
||||
Finding = meshconditions.Finding
|
||||
FindingCleared = meshconditions.FindingCleared
|
||||
FindingsStated = meshconditions.FindingsStated
|
||||
FindingsAsked = meshconditions.FindingsAskedEvent
|
||||
FindingsWriter = views.Writer[Finding, FindingCleared, FindingsStated, FindingsAsked]
|
||||
)
|
||||
|
||||
// idOf is a finding's id: the id of what it is about, and a ref that tells findings of one kind on one id apart after it
|
||||
// (`<id>.<ref>`, each escaped as a name of a key is; design 47 G A-14).
|
||||
func idOf(id, ref string) string {
|
||||
if ref == "" {
|
||||
return keys.EscapeName(id)
|
||||
}
|
||||
return keys.EscapeName(id) + "." + keys.EscapeName(ref)
|
||||
}
|
||||
|
||||
// finding is f about scope and id (with ref when findings of one kind on one id need telling apart), on machine: it sets
|
||||
// the key's scope and id and the subject's scope and id equal (design 47 G A-14).
|
||||
func finding(scope, id, ref, machine string, f Finding) Finding {
|
||||
f.Scope, f.ID = scope, idOf(id, ref)
|
||||
f.Subject = meshconditions.Subject{Scope: scope, ID: f.ID, Machine: machine}
|
||||
return f
|
||||
}
|
||||
|
||||
// said is the one observation a finding keeps as evidence; its moment is the finding's own (`at`), so that a finding
|
||||
// that says the same is not written again.
|
||||
func said(s string) []meshconditions.Evidence {
|
||||
return []meshconditions.Evidence{{Said: strings.TrimSpace(s)}}
|
||||
}
|
||||
|
||||
// keyOf is where f is kept: `<scope>.<id>.<kind>`.
|
||||
func keyOf(f Finding) string { return views.FindingKey(f.Scope, f.ID, f.Kind) }
|
||||
|
||||
// findingKey is the key of the finding of kind about scope and id with ref, as finding makes it.
|
||||
func findingKey(scope, id, ref, kind string) string {
|
||||
return views.FindingKey(scope, idOf(id, ref), kind)
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
@@ -65,18 +64,26 @@ func (r runtimeStore) Watch(state string, handle func(key string, value []byte,
|
||||
|
||||
// NewChecker is the self-check over the runtime.
|
||||
func NewChecker() *Checker {
|
||||
store := runtimeStore{views: newViews()}
|
||||
return &Checker{Store: store, Registry: Registry(), Now: time.Now, Logf: logf, Host: os.Getenv("MESH_NODE"),
|
||||
c := &Checker{Registry: Registry(), Now: time.Now, Logf: logf, Host: os.Getenv("MESH_NODE"),
|
||||
Enabled: enabledProbes,
|
||||
Ask: func(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) },
|
||||
Publish: stdio.SeatPublish}
|
||||
w, err := views.NewStdioWriter(views.WriterConfig[Finding, FindingCleared, FindingsStated, FindingsAsked]{
|
||||
Module: Module, State: stdio.State(FindingsState), Observe: c.Observe, Logf: logf})
|
||||
if err != nil {
|
||||
logf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
c.Writer = w
|
||||
c.Store = runtimeStore{views: newViews(w)}
|
||||
return c
|
||||
}
|
||||
|
||||
// newViews is the self-check's pictures of the indexer's and the assigner's states it probes (ADR 0615): each asked once of
|
||||
// its owner's read tool, then followed by the owner's events. A first view unanswered past its bound is a finding of the
|
||||
// self-check's own (first-view-unanswered), written into its state findings.
|
||||
func newViews() *views.Reader {
|
||||
r := &views.Reader{Module: Module, Logf: logf, Findings: meshconditionsclient.FindingWriter{State: stdio.State("findings")}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
|
||||
func newViews(w *FindingsWriter) *views.Reader {
|
||||
r := &views.Reader{Module: Module, Logf: logf, Findings: views.WriterFindings{Writer: w}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
|
||||
r.Follow(assignerModule, placing.StateAssignments, AssignmentsTool)
|
||||
r.Follow(assignerModule, placing.StateSeatHolders, SeatHoldersTool)
|
||||
r.Follow(indexerModule, "modules", ModulesTool)
|
||||
@@ -90,6 +97,9 @@ func main() {
|
||||
<-stdio.Served()
|
||||
c.Store.(runtimeStore).views.Start(context.Background())
|
||||
c.WatchKept(context.Background())
|
||||
if err := c.Writer.Start(context.Background()); err != nil {
|
||||
logf("the finding writer did not start: %v", err)
|
||||
}
|
||||
c.Keep(context.Background())
|
||||
}()
|
||||
if err := stdio.Serve("", append(Tools(c), c.Store.(runtimeStore).views.Tools("mesh_self_check")...)); err != nil {
|
||||
|
||||
@@ -140,7 +140,7 @@ func (c *Checker) streams(stream string) ([]streamRow, error) {
|
||||
}
|
||||
|
||||
// probeStreams is D7: every stream the mesh defines exists as defined.
|
||||
func probeStreams(ctx context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeStreams(ctx context.Context, c *Checker) ([]Finding, error) {
|
||||
rows, err := c.streams("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -149,19 +149,19 @@ func probeStreams(ctx context.Context, c *Checker) ([]meshconditionsclient.Obser
|
||||
for _, r := range rows {
|
||||
held[r.Name] = r
|
||||
}
|
||||
var out []meshconditionsclient.Observation
|
||||
var out []Finding
|
||||
for _, s := range buscompose.MeshStreams() {
|
||||
have, ok := held[s.Name]
|
||||
if !ok {
|
||||
out = append(out, meshconditionsclient.Observation{Ref: "stream-missing", Severity: meshconditionsclient.Urgent, Scope: meshconditionsclient.ScopeBus, ID: s.Name,
|
||||
Summary: fmt.Sprintf("the stream %s is not on the bus: %s", s.Name, firstLine(s.Why)),
|
||||
Said: "no stream " + s.Name})
|
||||
out = append(out, finding(meshconditionsclient.ScopeBus, s.Name, "stream-missing", "", Finding{Kind: "stream-wrong", Severity: meshconditionsclient.Urgent,
|
||||
Summary: fmt.Sprintf("the stream %s is not on the bus: %s", s.Name, firstLine(s.Why)),
|
||||
Evidence: said("no stream " + s.Name)}))
|
||||
continue
|
||||
}
|
||||
if differs := streamDiffers(s, have); differs != "" {
|
||||
out = append(out, meshconditionsclient.Observation{Ref: "stream-redefined", Severity: meshconditionsclient.Warning, Scope: meshconditionsclient.ScopeBus, ID: s.Name,
|
||||
Summary: fmt.Sprintf("the stream %s is not as the mesh defines it: %s", s.Name, differs),
|
||||
Said: differs})
|
||||
out = append(out, finding(meshconditionsclient.ScopeBus, s.Name, "stream-redefined", "", Finding{Kind: "stream-wrong", Severity: meshconditionsclient.Warning,
|
||||
Summary: fmt.Sprintf("the stream %s is not as the mesh defines it: %s", s.Name, differs),
|
||||
Evidence: said(differs)}))
|
||||
}
|
||||
}
|
||||
return sorted(out), nil
|
||||
@@ -213,7 +213,7 @@ type closedAnswer struct {
|
||||
}
|
||||
|
||||
// probeReconnects is D15: each user of the bus whose connections were dropped more than twelve times an hour.
|
||||
func probeReconnects(_ context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeReconnects(_ context.Context, c *Checker) ([]Finding, error) {
|
||||
raw, err := c.Ask(ClosedTool, map[string]any{"hours": 1})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the bus module did not answer %s: %w", ClosedTool, err)
|
||||
@@ -226,7 +226,7 @@ func probeReconnects(_ context.Context, c *Checker) ([]meshconditionsclient.Obse
|
||||
if hours <= 0 {
|
||||
hours = 1
|
||||
}
|
||||
var out []meshconditionsclient.Observation
|
||||
var out []Finding
|
||||
for _, u := range read.Users {
|
||||
if u.User == "" || strings.HasPrefix(u.User, "(") || float64(u.Dropped)/hours <= reconnectMax {
|
||||
continue // refused before it logged in is nobody's reconnect loop
|
||||
@@ -254,16 +254,14 @@ func probeReconnects(_ context.Context, c *Checker) ([]meshconditionsclient.Obse
|
||||
partial = " (at least: the server's record of closed connections does not reach back the whole hour)"
|
||||
}
|
||||
who, machine := busUserWords(u.User)
|
||||
out = append(out, meshconditionsclient.Observation{Kind: "bus-reconnects", Severity: meshconditionsclient.Warning,
|
||||
Scope: meshconditionsclient.ScopeBus, ID: u.User, Machine: machine,
|
||||
out = append(out, finding(meshconditionsclient.ScopeBus, u.User, "", machine, Finding{Kind: "bus-reconnects", Severity: meshconditionsclient.Warning,
|
||||
Summary: fmt.Sprintf("the bus dropped %s's connection %d times in the last hour%s, more than %d: a client "+
|
||||
"reconnecting in a loop", u.User, u.Dropped, partial, reconnectMax),
|
||||
Said: fmt.Sprintf("%d of %d closed connections dropped in %.0f h; by name %s; why %s", u.Dropped, u.Closed,
|
||||
hours, strings.Join(names, ", "), strings.Join(why, ", ")),
|
||||
Evidence: said(fmt.Sprintf("%d of %d closed connections dropped in %.0f h; by name %s; why %s", u.Dropped, u.Closed,
|
||||
hours, strings.Join(names, ", "), strings.Join(why, ", "))),
|
||||
Headline: capital(who) + " keeps losing the bus",
|
||||
Explanation: capital(fmt.Sprintf("%s lost its connection to the bus %d times in the last hour and connected "+
|
||||
"again each time. While it reconnects, what it says and what it is asked waits.", who, u.Dropped)),
|
||||
Resolved: "Resolved: " + who + " stays connected"})
|
||||
"again each time. While it reconnects, what it says and what it is asked waits.", who, u.Dropped))}))
|
||||
}
|
||||
return sorted(out), nil
|
||||
}
|
||||
@@ -290,12 +288,12 @@ func busUserWords(user string) (string, string) {
|
||||
|
||||
// probeWatchdogs is DW: the watchdogs answer. The self-check and the watchdogs watch each other: S10, the
|
||||
// watchman's pulse on `checked`, is the other half.
|
||||
func probeWatchdogs(_ context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeWatchdogs(_ context.Context, c *Checker) ([]Finding, error) {
|
||||
if _, err := c.Ask(SignalsVerb, map[string]any{}); err != nil {
|
||||
return []meshconditionsclient.Observation{{Ref: "silent", Severity: meshconditionsclient.Urgent, Scope: meshconditionsclient.ScopeCore, ID: "watchdogs",
|
||||
return []Finding{finding(meshconditionsclient.ScopeCore, "watchdogs", "", "", Finding{Kind: "watchdogs-silent", Severity: meshconditionsclient.Urgent,
|
||||
Summary: "the watchdogs do not answer: the watchman holding mesh-watchdogs did not answer signals, so no " +
|
||||
"late signal is being said",
|
||||
Said: firstLine(err.Error())}}, nil
|
||||
Evidence: said(firstLine(err.Error()))})}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -304,7 +302,7 @@ func probeWatchdogs(_ context.Context, c *Checker) ([]meshconditionsclient.Obser
|
||||
// machine and back. The mesh defines no durable consumer of its own since the controller ended: the account consumers
|
||||
// are the grants part's to make and judge (issue 1126). The question to the bus module is that request: an answer is its round trip,
|
||||
// and no answer is no pass.
|
||||
func probeBusHealth(ctx context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeBusHealth(ctx context.Context, c *Checker) ([]Finding, error) {
|
||||
rows, err := c.streams("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no request crossed the bus to its module and came back: %w", err)
|
||||
@@ -322,13 +320,13 @@ func probeBusHealth(ctx context.Context, c *Checker) ([]meshconditionsclient.Obs
|
||||
if len(problems) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []meshconditionsclient.Observation{{Ref: "unhealthy", Severity: meshconditionsclient.Urgent, Scope: meshconditionsclient.ScopeBus, ID: "mesh",
|
||||
Summary: "the bus is not healthy: " + strings.Join(problems, "; "), Said: strings.Join(problems, "; ")}}, nil
|
||||
return []Finding{finding(meshconditionsclient.ScopeBus, "mesh", "", "", Finding{Kind: "core-unhealthy", Severity: meshconditionsclient.Urgent,
|
||||
Summary: "the bus is not healthy: " + strings.Join(problems, "; "), Evidence: said(strings.Join(problems, "; "))})}, nil
|
||||
}
|
||||
|
||||
// sorted is findings in a stated order, so two runs over one mesh say the same.
|
||||
func sorted(out []meshconditionsclient.Observation) []meshconditionsclient.Observation {
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].Key() < out[j].Key() })
|
||||
func sorted(out []Finding) []Finding {
|
||||
sort.SliceStable(out, func(i, j int) bool { return keyOf(out[i]) < keyOf(out[j]) })
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ func (c *Checker) dataFacts(ctx context.Context) (dataFacts, error) {
|
||||
}
|
||||
|
||||
// probeData is D13.
|
||||
func probeData(ctx context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeData(ctx context.Context, c *Checker) ([]Finding, error) {
|
||||
f, err := c.dataFacts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -263,14 +263,13 @@ func firstSeen(r diskreadings.Reading, now time.Time) time.Time {
|
||||
return now
|
||||
}
|
||||
|
||||
func obs(kind string, id, machine string, sev string, summary, said string) meshconditionsclient.Observation {
|
||||
return meshconditionsclient.Observation{Kind: kind, Ref: kind, Severity: sev, Summary: summary, Said: said,
|
||||
Scope: ScopeMachine, ID: id, Machine: machine}
|
||||
func obs(kind string, id, machine string, sev string, summary, saying string) Finding {
|
||||
return finding(ScopeMachine, id, "", machine, Finding{Kind: kind, Severity: sev, Summary: summary, Evidence: said(saying)})
|
||||
}
|
||||
|
||||
// dataFindings is every condition the data on record raises now: a function of what is known, as the controller's was.
|
||||
func dataFindings(f dataFacts) []meshconditionsclient.Observation {
|
||||
var out []meshconditionsclient.Observation
|
||||
func dataFindings(f dataFacts) []Finding {
|
||||
var out []Finding
|
||||
arrays := map[string][]diskreadings.Reading{}
|
||||
type shrunk struct {
|
||||
machine, dataset, class string
|
||||
@@ -301,7 +300,6 @@ func dataFindings(f dataFacts) []meshconditionsclient.Observation {
|
||||
"`cleanup delete %s %s %s --why …` once a person has decided, or assign %s there again",
|
||||
r.ID, r.Module, r.Node, r.Class, sizeWords(r.SizeBytes), int(f.now.Sub(r.RetiredAt).Hours()/24),
|
||||
r.RetiredWhy, r.Node, r.Module, r.ID, r.Module), "kept at "+orUnknownPath(r.Path)))
|
||||
out[len(out)-1].Ref = "cleanup"
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -428,7 +426,7 @@ func dataFindings(f dataFacts) []meshconditionsclient.Observation {
|
||||
fmt.Sprintf("%s of %s on %s holds %s, and the copy %s kept on %s holds %s: %s is running on "+
|
||||
"an empty replacement of its data. Move the data, or assign it back where its data is",
|
||||
a.ID, a.Module, a.Node, sizeWords(a.SizeBytes), o.Module, o.Node, sizeWords(o.SizeBytes), a.Module), "")
|
||||
ob.Also = []string{o.Node}
|
||||
ob.Subject.Also = []string{o.Node}
|
||||
out = append(out, ob)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -55,39 +55,37 @@ func mibWords(b uint64) string { return fmt.Sprintf("%.1f MiB", float64(b)/(1024
|
||||
|
||||
// fillKey is where a bucket's or log's fill finding is kept.
|
||||
func fillKey(stream string) string {
|
||||
return meshconditionsclient.Observation{Scope: meshconditionsclient.ScopeBus, ID: stream, Ref: "filling"}.Key()
|
||||
return findingKey(meshconditionsclient.ScopeBus, stream, "", "bucket-or-log-filling")
|
||||
}
|
||||
|
||||
// fillObservation is what one bucket's or log's fill says: a finding at fillRaiseAt or above, and, while its finding
|
||||
// is written, at fillClearBelow or above too; nothing otherwise, which is what clears it.
|
||||
func fillObservation(stream string, r fillRow, open bool) (meshconditionsclient.Observation, bool) {
|
||||
func fillObservation(stream string, r fillRow, open bool) (Finding, bool) {
|
||||
module, kind, name, ok := bucketOrLog(stream)
|
||||
if !ok || r.MaxBytes == 0 {
|
||||
return meshconditionsclient.Observation{}, false // no module's, or with no cap: it cannot fill
|
||||
return Finding{}, false // no module's, or with no cap: it cannot fill
|
||||
}
|
||||
// Compared in bytes, not rounded percent: 74.9% is not 75%.
|
||||
atRaise := r.Bytes*100 >= r.MaxBytes*fillRaiseAt
|
||||
aboveClear := r.Bytes*100 >= r.MaxBytes*fillClearBelow
|
||||
if !atRaise && !(open && aboveClear) {
|
||||
return meshconditionsclient.Observation{}, false
|
||||
return Finding{}, false
|
||||
}
|
||||
pct := r.Bytes * 100 / r.MaxBytes
|
||||
return meshconditionsclient.Observation{Kind: "bucket-or-log-filling", Ref: "filling", Severity: meshconditionsclient.Warning,
|
||||
Scope: meshconditionsclient.ScopeBus, ID: stream,
|
||||
return finding(meshconditionsclient.ScopeBus, stream, "", "", Finding{Kind: "bucket-or-log-filling", Severity: meshconditionsclient.Warning,
|
||||
Summary: fmt.Sprintf("%s's %s %s holds %s of %s, %d%%: at its cap the bus refuses the module's next write",
|
||||
module, kind, name, mibWords(r.Bytes), mibWords(r.MaxBytes), pct),
|
||||
Said: fmt.Sprintf("module %s, %s %s (stream %s): %d of %d bytes, %d%%", module, kind, name, stream, r.Bytes,
|
||||
r.MaxBytes, pct),
|
||||
Evidence: said(fmt.Sprintf("module %s, %s %s (stream %s): %d of %d bytes, %d%%", module, kind, name, stream, r.Bytes,
|
||||
r.MaxBytes, pct)),
|
||||
Headline: "A module's bucket or log on the bus is filling up",
|
||||
Needs: "decide whether to raise its cap or have the module keep less.",
|
||||
Explanation: "A bucket or log a module keeps on the bus holds three quarters of its cap or more. When it is " +
|
||||
"full, the bus refuses what the module writes there.",
|
||||
Resolved: "It has room again"}, true
|
||||
"full, the bus refuses what the module writes there."}), true
|
||||
}
|
||||
|
||||
// probeFill is D-fill: every module's bucket and log on the bus judged against its cap. The bus module not answering
|
||||
// is the probe failing to run, which clears nothing; a finding already written counts as open.
|
||||
func probeFill(_ context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeFill(_ context.Context, c *Checker) ([]Finding, error) {
|
||||
raw, err := c.Ask(StreamsTool, map[string]any{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the bus module did not answer %s: %w", StreamsTool, err)
|
||||
@@ -103,7 +101,7 @@ func probeFill(_ context.Context, c *Checker) ([]meshconditionsclient.Observatio
|
||||
return nil, fmt.Errorf("its own findings cannot be read, so a fill between the thresholds cannot be judged: %w", err)
|
||||
}
|
||||
sort.Slice(a.Detail, func(i, j int) bool { return a.Detail[i].Name < a.Detail[j].Name })
|
||||
var out []meshconditionsclient.Observation
|
||||
var out []Finding
|
||||
for _, r := range a.Detail {
|
||||
if o, said := fillObservation(r.Name, r, written.has(fillKey(r.Name))); said {
|
||||
out = append(out, o)
|
||||
|
||||
@@ -128,7 +128,7 @@ var lookupHost = func(ctx context.Context, host string) ([]string, error) {
|
||||
// probeBans is D8: no address the mesh owns is in any machine's ban list. The mesh's own addresses are every machine's
|
||||
// private address and the address its endpoint names, from assignments; the ban lists are asked of each machine's holder
|
||||
// of node-intrusion-prevention that is heard from.
|
||||
func probeBans(ctx context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeBans(ctx context.Context, c *Checker) ([]Finding, error) {
|
||||
places, err := c.assignments()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -183,7 +183,7 @@ func probeBans(ctx context.Context, c *Checker) ([]meshconditionsclient.Observat
|
||||
}(i, node)
|
||||
}
|
||||
wg.Wait()
|
||||
var out []meshconditionsclient.Observation
|
||||
var out []Finding
|
||||
var unasked []string
|
||||
for i, node := range holders {
|
||||
if errs[i] != nil {
|
||||
@@ -206,16 +206,14 @@ func probeBans(ctx context.Context, c *Checker) ([]meshconditionsclient.Observat
|
||||
}
|
||||
}
|
||||
if len(banned) > 0 {
|
||||
out = append(out, meshconditionsclient.Observation{Kind: "own-address-banned", Ref: "bans-the-mesh", Severity: meshconditionsclient.Urgent,
|
||||
Scope: ScopeMachine, ID: node, Machine: node,
|
||||
out = append(out, finding(ScopeMachine, node, "", node, Finding{Kind: "own-address-banned", Severity: meshconditionsclient.Urgent,
|
||||
Summary: fmt.Sprintf("%s's ban list holds %d of the mesh's own address(es) — %s: the mesh is locked out "+
|
||||
"of itself there (ADR 0186)", node, len(banned), strings.Join(whose, ", ")),
|
||||
Said: strings.Join(banned, ", "),
|
||||
Evidence: said(strings.Join(banned, ", ")),
|
||||
Headline: node + " has banned the mesh",
|
||||
Needs: fmt.Sprintf("lift the ban on %s, or wait for it to expire.", node),
|
||||
Explanation: fmt.Sprintf("The intrusion protection on %s banned one of the mesh's own addresses, so the "+
|
||||
"mesh is locked out of it.", node),
|
||||
Resolved: node + " no longer bans the mesh"})
|
||||
"mesh is locked out of it.", node)}))
|
||||
}
|
||||
}
|
||||
if len(unasked) > 0 {
|
||||
|
||||
@@ -205,7 +205,7 @@ func (c *Checker) confined(node string, place placing.Assignments, heard bool) (
|
||||
|
||||
// probeAgentAccounts is DA: every machine that names an agent account has it judged, on its node-engine's newest
|
||||
// statement, unable to become root without a person (ADR 0266).
|
||||
func probeAgentAccounts(_ context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
func probeAgentAccounts(_ context.Context, c *Checker) ([]Finding, error) {
|
||||
places, err := c.assignments()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -219,7 +219,7 @@ func probeAgentAccounts(_ context.Context, c *Checker) ([]meshconditionsclient.O
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
var out []meshconditionsclient.Observation
|
||||
var out []Finding
|
||||
for _, n := range nodes {
|
||||
if places[n].Decided.AgentAccount == "" {
|
||||
continue
|
||||
@@ -233,17 +233,15 @@ func probeAgentAccounts(_ context.Context, c *Checker) ([]meshconditionsclient.O
|
||||
if v.Confined || v.Quiet {
|
||||
continue
|
||||
}
|
||||
out = append(out, meshconditionsclient.Observation{Kind: "agent-can-become-root", Ref: "agent-root", Severity: meshconditionsclient.Urgent,
|
||||
Scope: ScopeMachine, ID: n, Machine: n,
|
||||
out = append(out, finding(ScopeMachine, n, "", n, Finding{Kind: "agent-can-become-root", Severity: meshconditionsclient.Urgent,
|
||||
Summary: fmt.Sprintf("on %s, %s (ADR 0266): an agent there may become root without a person, and no "+
|
||||
"answer from a channel authorises an act there (ADR 0259 §8)", n, v.Why),
|
||||
Said: v.Why,
|
||||
Evidence: said(v.Why),
|
||||
Headline: "Sessions on " + n + " could become root",
|
||||
Needs: "take the sessions' own account out of every group and rule that grants root; the details say which.",
|
||||
Explanation: fmt.Sprintf("Assistant sessions on %s run under an account of their own, so that none "+
|
||||
"can take over the machine without you. The machine cannot show that this holds now, so an answer "+
|
||||
"from your phone authorises nothing there until it does.", n),
|
||||
Resolved: "Sessions on " + n + " cannot become root again"})
|
||||
"from your phone authorises nothing there until it does.", n)}))
|
||||
}
|
||||
return sorted(out), nil
|
||||
}
|
||||
|
||||
@@ -107,10 +107,10 @@ func (b bus) ask(key string, body any) (json.RawMessage, error) {
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
|
||||
func keysOf(found []meshconditionsclient.Observation) []string {
|
||||
func keysOf(found []Finding) []string {
|
||||
var out []string
|
||||
for _, f := range found {
|
||||
out = append(out, f.Key())
|
||||
out = append(out, keyOf(f))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func keysOf(found []meshconditionsclient.Observation) []string {
|
||||
func TestTheBusProbes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
probe func(context.Context, *Checker) ([]meshconditionsclient.Observation, error)
|
||||
probe func(context.Context, *Checker) ([]Finding, error)
|
||||
bus bus
|
||||
want []string
|
||||
err string
|
||||
@@ -128,24 +128,24 @@ func TestTheBusProbes(t *testing.T) {
|
||||
{"D7 all as defined", probeStreams, bus{}, nil, "", nil},
|
||||
{"D7 missing and redefined", probeStreams, bus{drop: map[string]bool{"NODES": true}, change: func(rows map[string]*streamRow) {
|
||||
rows["EVENTS"].Retention = "workqueue"
|
||||
}}, []string{"bus.EVENTS.stream-redefined", "bus.NODES.stream-missing"}, "", nil},
|
||||
}}, []string{"bus.EVENTS.stream-redefined.stream-wrong", "bus.NODES.stream-missing.stream-wrong"}, "", nil},
|
||||
{"D15 quiet", probeReconnects, bus{}, nil, "", nil},
|
||||
{"D15 a loop", probeReconnects, bus{closed: `{"hours":1,"reaches":true,"users":[
|
||||
{"user":"g14.node-tools","closed":30,"dropped":20,"names":[{"name":"node-tools","dropped":20,"reasons":{"Stale Connection":20}}]},
|
||||
{"user":"ace.records","closed":5,"dropped":3},{"user":"(unauthenticated)","closed":90,"dropped":90}]}`},
|
||||
[]string{"bus.g14.node-tools.bus-reconnects"}, "", nil},
|
||||
[]string{"bus.g14=2enode-tools.bus-reconnects"}, "", nil},
|
||||
{"DW answers", probeWatchdogs, bus{}, nil, "", nil},
|
||||
{"DW silent", probeWatchdogs, bus{signals: errors.New("no responders")}, []string{"core.watchdogs.silent"}, "", nil},
|
||||
{"DW silent", probeWatchdogs, bus{signals: errors.New("no responders")}, []string{"core.watchdogs.watchdogs-silent"}, "", nil},
|
||||
{"H-bus healthy", probeBusHealth, bus{}, nil, "", nil},
|
||||
{"H-bus a stream missing", probeBusHealth,
|
||||
bus{drop: map[string]bool{"NODES": true}}, []string{"bus.mesh.unhealthy"}, "", nil},
|
||||
bus{drop: map[string]bool{"NODES": true}}, []string{"bus.mesh.core-unhealthy"}, "", nil},
|
||||
{"H-bus no round trip", probeBusHealth, bus{asksFail: errors.New("timeout")}, nil, "no request crossed", nil},
|
||||
// D8 (issue 238): the assignments' addresses and endpoints against each heard holder's ban list.
|
||||
{"D8 nothing of the mesh banned", probeBans, bus{banned: map[string]string{"ace": `{"banned":["198.51.100.7"]}`,
|
||||
"novox": `{"banned":[]}`}}, nil, "", machines()},
|
||||
{"D8 a private address and an endpoint banned", probeBans, bus{banned: map[string]string{
|
||||
"ace": `{"jails":{"sshd":["10.10.0.1","198.51.100.7"]}}`, "novox": `{"banned":["203.0.113.5"]}`}},
|
||||
[]string{"machine.ace.bans-the-mesh", "machine.novox.bans-the-mesh"}, "", machines()},
|
||||
[]string{"machine.ace.own-address-banned", "machine.novox.own-address-banned"}, "", machines()},
|
||||
{"D8 a machine not heard from is not asked", probeBans, bus{banned: map[string]string{"novox": `{"banned":[]}`,
|
||||
"ace": `{"banned":[]}`}}, nil, "", machines()},
|
||||
{"D8 a heard holder that does not answer is no pass", probeBans, bus{banned: map[string]string{}}, nil,
|
||||
@@ -154,9 +154,9 @@ func TestTheBusProbes(t *testing.T) {
|
||||
// the manifests declare; the kinds, keys and words are the controller's.
|
||||
{"D13 all held", probeData, bus{}, nil, "", dataMesh(func(*dataRig) {})},
|
||||
{"D13 shrank, gone, quiet, unprotected and stale", probeData, bus{}, []string{
|
||||
"machine.ace.pg.data.data-quiet", "machine.ace.pg.data.backup-stale", "machine.ace.pg.dumps.data-missing",
|
||||
"machine.ace.plex.movies.protection-missing", "machine.ace.plex.thumbs.data-shrank",
|
||||
"machine.ace.array.md-md127.array-degraded"},
|
||||
"machine.ace=2epg=2edata.data-quiet", "machine.ace=2epg=2edata.backup-stale", "machine.ace=2epg=2edumps.data-missing",
|
||||
"machine.ace=2eplex=2emovies.protection-missing", "machine.ace=2eplex=2ethumbs.data-shrank",
|
||||
"machine.ace=2earray=2emd-md127.array-degraded"},
|
||||
"", dataMesh(func(r *dataRig) {
|
||||
r.size("ace", "pg", "data", 100<<20, 100<<20).write("ace", "pg", "data", -80*time.Hour)
|
||||
r.gone("ace", "pg", "dumps")
|
||||
@@ -165,23 +165,23 @@ func TestTheBusProbes(t *testing.T) {
|
||||
r.size("ace", "plex", "library", 1<<30, 1<<30).array("ace", "plex", "library", "md", "md127", false)
|
||||
r.lastGood("ace", "pg", "data", -100*time.Hour)
|
||||
})},
|
||||
{"D13 a dataset shrank once for its items", probeData, bus{}, []string{"machine.ace.dataset.storage-media.data-shrank"},
|
||||
{"D13 a dataset shrank once for its items", probeData, bus{}, []string{"machine.ace=2edataset=2estorage-media.data-shrank"},
|
||||
"", dataMesh(func(r *dataRig) {
|
||||
r.size("ace", "plex", "thumbs", 10<<20, 600<<20).dataset("ace", "plex", "thumbs", "storage/media")
|
||||
r.size("ace", "plex", "meta", 10<<20, 600<<20).dataset("ace", "plex", "meta", "storage/media")
|
||||
})},
|
||||
{"D13 a retired item is kept, not judged, and cleanup waits after thirty days", probeData, bus{}, []string{
|
||||
"machine.ace.pg.dumps.cleanup"}, "", dataMesh(func(r *dataRig) {
|
||||
"machine.ace=2epg=2edumps.cleanup-waiting"}, "", dataMesh(func(r *dataRig) {
|
||||
r.size("ace", "pg", "dumps", 1<<20, 1<<30).retired("ace", "pg", "dumps", -31*24*time.Hour)
|
||||
r.size("ace", "pg", "old", 1<<20, 1<<30).retired("ace", "pg", "old", -2*24*time.Hour)
|
||||
})},
|
||||
{"D13 a copy far smaller than a retired one elsewhere is an empty replacement", probeData, bus{},
|
||||
[]string{"machine.ace.plex.thumbs.empty-replacement"}, "", dataMesh(func(r *dataRig) {
|
||||
[]string{"machine.ace=2eplex=2ethumbs.empty-replacement"}, "", dataMesh(func(r *dataRig) {
|
||||
r.size("ace", "plex", "thumbs", 10<<20, 10<<20)
|
||||
r.size("g14", "plex", "thumbs", 600<<20, 600<<20).retired("g14", "plex", "thumbs", -24*time.Hour)
|
||||
})},
|
||||
{"D13 no good backup is counted from when the item was first measured", probeData, bus{},
|
||||
[]string{"machine.ace.pg.data.backup-stale"}, "", dataMesh(func(r *dataRig) {
|
||||
[]string{"machine.ace=2epg=2edata.backup-stale"}, "", dataMesh(func(r *dataRig) {
|
||||
delete(r.good, diskreadings.ReadingKey("ace", "pg", "data"))
|
||||
delete(r.good, diskreadings.ReadingKey("ace", "plex", "meta"))
|
||||
r.firstSeen("ace", "pg", "data", -100*time.Hour).firstSeen("ace", "plex", "meta", -time.Hour)
|
||||
@@ -193,24 +193,24 @@ func TestTheBusProbes(t *testing.T) {
|
||||
{Name: "LOG_mesh-delivery-builder_build-log", Bytes: 10, MaxBytes: 100}, {Name: "KV_nocap_x", Bytes: 999}}}, nil, "", nil},
|
||||
{"D-fill a bucket and a log filling", probeFill, bus{fills: []fillRow{{Name: "KV_mesh-issues_issues", Bytes: 75, MaxBytes: 100},
|
||||
{Name: "LOG_mesh-delivery-builder_build-log", Bytes: 99, MaxBytes: 100}}},
|
||||
[]string{"bus.KV_mesh-issues_issues.filling", "bus.LOG_mesh-delivery-builder_build-log.filling"}, "", nil},
|
||||
[]string{"bus.KV_mesh-issues_issues.bucket-or-log-filling", "bus.LOG_mesh-delivery-builder_build-log.bucket-or-log-filling"}, "", nil},
|
||||
{"D-fill between the thresholds raises nothing new", probeFill,
|
||||
bus{fills: []fillRow{{Name: "KV_mesh-issues_issues", Bytes: 72, MaxBytes: 100}}}, nil, "", nil},
|
||||
{"D-fill between the thresholds keeps what is said", probeFill,
|
||||
bus{fills: []fillRow{{Name: "KV_mesh-issues_issues", Bytes: 72, MaxBytes: 100}}},
|
||||
[]string{"bus.KV_mesh-issues_issues.filling"}, "", filling("KV_mesh-issues_issues")},
|
||||
[]string{"bus.KV_mesh-issues_issues.bucket-or-log-filling"}, "", filling("KV_mesh-issues_issues")},
|
||||
{"D-fill the bus module silent", probeFill, bus{asksFail: errors.New("no responders")}, nil, "did not answer", nil},
|
||||
// DA (ADR 0266): the agent account each machine names, judged on its engine's report while it is heard from.
|
||||
{"DA confined", probeAgentAccounts, bus{}, nil, "", agents(map[string]string{"novox": stateHealthy})},
|
||||
{"DA a way to root found", probeAgentAccounts, bus{}, []string{"machine.novox.agent-root"}, "",
|
||||
{"DA a way to root found", probeAgentAccounts, bus{}, []string{"machine.novox.agent-can-become-root"}, "",
|
||||
agents(map[string]string{"novox": stateUnhealthy})},
|
||||
{"DA no verdict on it", probeAgentAccounts, bus{}, []string{"machine.novox.agent-root"}, "",
|
||||
{"DA no verdict on it", probeAgentAccounts, bus{}, []string{"machine.novox.agent-can-become-root"}, "",
|
||||
agents(map[string]string{"novox": ""})},
|
||||
{"DA the search runs, within its quiet", probeAgentAccounts, bus{}, nil, "",
|
||||
agents(map[string]string{"novox": "pending"})},
|
||||
{"DA a machine not heard from has no verdict on now", probeAgentAccounts, bus{}, []string{"machine.g14.agent-root"},
|
||||
{"DA a machine not heard from has no verdict on now", probeAgentAccounts, bus{}, []string{"machine.g14.agent-can-become-root"},
|
||||
"", agents(map[string]string{"novox": stateHealthy, "g14": stateHealthy})},
|
||||
{"DA an engine older than the judging", probeAgentAccounts, bus{}, []string{"machine.novox.agent-root"}, "",
|
||||
{"DA an engine older than the judging", probeAgentAccounts, bus{}, []string{"machine.novox.agent-can-become-root"}, "",
|
||||
agents(map[string]string{"novox": "old"})},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
@@ -301,10 +301,10 @@ func placed(s *memStore) map[string]placing.Assignments {
|
||||
// filling is a store holding the self-check's written fill finding of a stream: the condition open.
|
||||
func filling(stream string) *memStore {
|
||||
s := newStore()
|
||||
o := meshconditionsclient.Observation{Kind: "bucket-or-log-filling", Ref: "filling", Severity: meshconditionsclient.Warning, Scope: meshconditionsclient.ScopeBus, ID: stream,
|
||||
Summary: "filling", Source: "D-fill"}
|
||||
raw, _ := json.Marshal(o.Finding(Module, "run", time.Now()))
|
||||
_ = s.Put(FindingsState, o.Key(), json.RawMessage(raw))
|
||||
o := finding(meshconditionsclient.ScopeBus, stream, "", "", Finding{Kind: "bucket-or-log-filling", Severity: meshconditionsclient.Warning,
|
||||
Summary: "filling", Source: "D-fill", Observer: Module, Run: "run", At: time.Now(), Since: time.Now()})
|
||||
raw, _ := json.Marshal(o)
|
||||
_ = s.Put(FindingsState, keyOf(o), json.RawMessage(raw))
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
// The self-check (novox/hq ADR 0564 §6): the registry of probes, each a live invariant of a design, run every five
|
||||
// minutes, each probe within thirty seconds. What a probe finds is written as a finding in this module's bucket
|
||||
// `findings`, which the condition keeper raises and clears conditions from (ADR 0564 §2); every run is kept in the
|
||||
// `findings`, through mesh-sdk's finding writer, which the condition keeper raises and clears conditions from (ADR 0564 §2); every run is kept in the
|
||||
// bucket `verdicts` for a week and the last as the key `last` of `verdict`, and is said as `checked`, which the
|
||||
// watchman and mesh-watcher hear (S10, ADR 0227 §6). A probe that errors or does not finish is never a pass: it is
|
||||
// said in the verdict at once and written as a finding of its own when the next run cannot run it either.
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
meshselfcheck "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshselfcheck"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// Module and Seat are this module and the seat it holds once for the mesh.
|
||||
@@ -95,7 +96,7 @@ type Probe struct {
|
||||
// Deferred names the facts it lacks on the bus, for a probe whose facts have no home there yet (ADR 0564 §6):
|
||||
// answered deferred, never a pass.
|
||||
Deferred string
|
||||
Run func(ctx context.Context, c *Checker) ([]meshconditionsclient.Observation, error)
|
||||
Run func(ctx context.Context, c *Checker) ([]Finding, error)
|
||||
}
|
||||
|
||||
// ProbeVerdict, Counts and Run are a run as the seat mesh-self-check's contract holds it: the body of `checked`, the controller's
|
||||
@@ -119,6 +120,9 @@ type Checker struct {
|
||||
Logf func(format string, args ...any)
|
||||
Host string
|
||||
Registry []Probe
|
||||
// Writer is the finding writer over this module's state `findings` (views.NewStdioWriter; design 47 G A-16), whose
|
||||
// observe is Observe.
|
||||
Writer *FindingsWriter
|
||||
|
||||
running sync.Mutex
|
||||
mu sync.Mutex
|
||||
@@ -144,14 +148,36 @@ func (c *Checker) LastRun() *Run {
|
||||
}
|
||||
|
||||
type result struct {
|
||||
found []meshconditionsclient.Observation
|
||||
found []Finding
|
||||
err error
|
||||
took time.Duration
|
||||
}
|
||||
|
||||
// RunOnce runs every probe of the registry, writes what changed in `findings`, keeps the verdict and says `checked`.
|
||||
// One run at a time: a person's run during a scheduled one waits for it.
|
||||
// The context a run is asked with: why it runs, and where its verdict is answered.
|
||||
type (
|
||||
whyKey struct{}
|
||||
runOut struct{}
|
||||
)
|
||||
|
||||
// RunOnce runs every probe of the registry through the findings writer, which writes what changed in `findings` and says
|
||||
// `findings-stated` (design 47 G §461, §485), keeps the verdict and says `checked`. One run at a time: a person's run
|
||||
// during a scheduled one waits for it.
|
||||
func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
var out Run
|
||||
ctx = context.WithValue(context.WithValue(ctx, whyKey{}, why), runOut{}, &out)
|
||||
if err := c.Writer.State(ctx, ""); err != nil {
|
||||
c.Logf("[%s] what this run found is not all written: %v", Module, err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Observe is the writer's observe: it runs every probe, keeps the verdict and says `checked`, and answers the findings
|
||||
// this run observes and the keys it cannot judge (those of a probe that could not run).
|
||||
func (c *Checker) Observe(ctx context.Context) (views.Observed[Finding], error) {
|
||||
why, _ := ctx.Value(whyKey{}).(string)
|
||||
if why == "" {
|
||||
why = "a statement of the findings"
|
||||
}
|
||||
c.running.Lock()
|
||||
defer c.running.Unlock()
|
||||
c.mu.Lock()
|
||||
@@ -179,9 +205,9 @@ func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
|
||||
written, readErr := c.written()
|
||||
seen := map[string]bool{}
|
||||
want := map[string]meshconditionsclient.Observation{} // every finding this run writes, by key
|
||||
ran := map[string]bool{} // the sources whose findings this run judged whole
|
||||
var blind []meshconditionsclient.Observation
|
||||
want := map[string]Finding{} // every finding this run observes, by key
|
||||
ran := map[string]bool{} // the sources whose findings this run judged whole
|
||||
var blind []Finding
|
||||
for i, p := range c.Registry {
|
||||
if !on(p.ID) {
|
||||
// Off by the setting: it ran nothing and sees nothing, so what it raised before is cleared, and it has no
|
||||
@@ -199,10 +225,10 @@ func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
case r.err != nil:
|
||||
v.Verdict, v.Error = VerdictFailedToRun, r.err.Error()
|
||||
run.Counts.FailedToRun++
|
||||
blind = append(blind, meshconditionsclient.Observation{Kind: "probe-failed", Ref: "failed", Severity: meshconditionsclient.Warning,
|
||||
Scope: meshconditionsclient.ScopeProbe, ID: p.ID, Source: sourceSelf,
|
||||
Summary: fmt.Sprintf("the probe %s (%s) could not run: what it checks is not known — never a pass", p.ID, p.Asserts),
|
||||
Said: firstLine(r.err.Error())})
|
||||
blind = append(blind, finding(meshconditionsclient.ScopeProbe, p.ID, "", "", Finding{Kind: "probe-failed",
|
||||
Severity: meshconditionsclient.Warning, Source: sourceSelf,
|
||||
Summary: fmt.Sprintf("the probe %s (%s) could not run: what it checks is not known — never a pass", p.ID, p.Asserts),
|
||||
Evidence: said(firstLine(r.err.Error()))}))
|
||||
default:
|
||||
ran[p.ID] = true
|
||||
for _, f := range r.found {
|
||||
@@ -211,7 +237,7 @@ func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
}
|
||||
f.Source = p.ID
|
||||
f = worded(f)
|
||||
key := f.Key()
|
||||
key := keyOf(f)
|
||||
seen[key] = true
|
||||
if confirm(f) && !c.seenBefore(key) && !written.has(key) {
|
||||
v.Unconfirmed = append(v.Unconfirmed, f.Summary)
|
||||
@@ -236,18 +262,12 @@ func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
ran[sourceSelf] = true
|
||||
for _, f := range blind {
|
||||
f = worded(f)
|
||||
key := f.Key()
|
||||
key := keyOf(f)
|
||||
seen[key] = true
|
||||
if c.seenBefore(key) || written.has(key) {
|
||||
want[key] = f
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
// Blind to its own bucket: nothing is written or deleted (ADR 0227 §4), and the log says so.
|
||||
c.Logf("[%s] its findings cannot be read, so none is written or cleared this run: %v", Module, readErr)
|
||||
} else if err := c.write(written, want, ran, run.Run); err != nil {
|
||||
c.Logf("[%s] what this run found is not all written: %v", Module, err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.seen = seen
|
||||
c.mu.Unlock()
|
||||
@@ -258,7 +278,32 @@ func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
run.Unsaid = int64(c.unsaid)
|
||||
c.mu.Unlock()
|
||||
c.keep(run)
|
||||
return run
|
||||
if out, ok := ctx.Value(runOut{}).(*Run); ok {
|
||||
*out = run
|
||||
}
|
||||
if readErr != nil {
|
||||
// Blind to its own findings: no set is said, so nothing is written or cleared (ADR 0227 §4).
|
||||
return views.Observed[Finding]{}, fmt.Errorf("its findings cannot be read, so none is written or cleared this run: %w", readErr)
|
||||
}
|
||||
keys := make([]string, 0, len(want))
|
||||
for k := range want {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
obs := views.Observed[Finding]{}
|
||||
for _, k := range keys {
|
||||
f := want[k]
|
||||
f.Run = run.Run
|
||||
obs.Findings = append(obs.Findings, f)
|
||||
}
|
||||
// A finding of a source that could not run is left as it is: a blind observer clears nothing (ADR 0227 §4).
|
||||
for k, f := range written {
|
||||
if _, still := want[k]; !still && !ran[f.Source] {
|
||||
obs.Pending = append(obs.Pending, views.PendingKey{Key: k, Since: f.Since, Bound: Every.String()})
|
||||
}
|
||||
}
|
||||
sort.Slice(obs.Pending, func(i, j int) bool { return obs.Pending[i].Key < obs.Pending[j].Key })
|
||||
return obs, nil
|
||||
}
|
||||
|
||||
// probe runs one probe within its bound; a panic or an overrun is an error, never a pass.
|
||||
@@ -291,8 +336,8 @@ func (c *Checker) seenBefore(key string) bool {
|
||||
return c.seen[key]
|
||||
}
|
||||
|
||||
// writtenFindings are the findings in the bucket, by key.
|
||||
type writtenFindings map[string]meshconditions.FindingsByKeyValue
|
||||
// writtenFindings are the findings in the state, by key.
|
||||
type writtenFindings map[string]Finding
|
||||
|
||||
func (w writtenFindings) has(key string) bool { _, ok := w[key]; return ok }
|
||||
|
||||
@@ -311,7 +356,7 @@ func (c *Checker) written() (writtenFindings, error) {
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
var f meshconditions.FindingsByKeyValue
|
||||
var f Finding
|
||||
if err := json.Unmarshal(raw, &f); err != nil {
|
||||
return out, fmt.Errorf("the finding %s is not readable: %w", k, err)
|
||||
}
|
||||
@@ -320,43 +365,6 @@ func (c *Checker) written() (writtenFindings, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// write puts each finding whose words changed and deletes each one a source that ran whole no longer sees. A finding
|
||||
// of a source that could not run is left as it is: a blind observer clears nothing (ADR 0227 §4).
|
||||
func (c *Checker) write(written writtenFindings, want map[string]meshconditionsclient.Observation, ran map[string]bool, run string) error {
|
||||
var errs []string
|
||||
keys := make([]string, 0, len(want))
|
||||
for k := range want {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
now := c.Now()
|
||||
for _, k := range keys {
|
||||
f := want[k].Finding(Module, run, now)
|
||||
if was, ok := written[k]; ok && meshconditionsclient.Says(was) == meshconditionsclient.Says(f) {
|
||||
continue
|
||||
}
|
||||
raw, err := json.Marshal(f)
|
||||
if err == nil {
|
||||
err = c.Store.Put(FindingsState, k, json.RawMessage(raw))
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, k+": "+err.Error())
|
||||
}
|
||||
}
|
||||
for k, f := range written {
|
||||
if _, still := want[k]; still || !ran[f.Source] {
|
||||
continue
|
||||
}
|
||||
if err := c.Store.Delete(FindingsState, k); err != nil {
|
||||
errs = append(errs, k+": "+err.Error())
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// keep keeps the run as the last verdict and in the week's verdicts, and says it as `checked`. Not said is said in
|
||||
// the log: the watchman's S10 says the self-check silent when `checked` stops.
|
||||
func (c *Checker) keep(run Run) {
|
||||
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// memStore is the bus's buckets in memory, counting the writes and the reads one key at a time, and handing every
|
||||
@@ -25,6 +26,7 @@ type memStore struct {
|
||||
gets map[string]int
|
||||
watches map[string][]func(key string, value []byte, gone bool)
|
||||
keysErr error
|
||||
events []string // the subjects the finding writer said its events on
|
||||
}
|
||||
|
||||
func newStore() *memStore {
|
||||
@@ -97,6 +99,22 @@ func (s *memStore) findingKeys() []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
// findingState is the writer's view of the store's state `findings`.
|
||||
type findingState struct{ s *memStore }
|
||||
|
||||
func (f findingState) Get(key string) (*stdio.StateEntry, error) {
|
||||
raw, ok, err := f.s.Get(FindingsState, key)
|
||||
if err != nil || !ok {
|
||||
return nil, err
|
||||
}
|
||||
return &stdio.StateEntry{Key: key, Value: raw}, nil
|
||||
}
|
||||
func (f findingState) Put(key string, value any) (uint64, error) {
|
||||
return 0, f.s.Put(FindingsState, key, value)
|
||||
}
|
||||
func (f findingState) Delete(key string) error { return f.s.Delete(FindingsState, key) }
|
||||
func (f findingState) Keys() ([]string, error) { return f.s.Keys(FindingsState) }
|
||||
|
||||
// checker is a self-check over memory, with a registry the test drives.
|
||||
func checker(store *memStore, registry ...Probe) (*Checker, *[]string) {
|
||||
var said []string
|
||||
@@ -108,6 +126,17 @@ func checker(store *memStore, registry ...Probe) (*Checker, *[]string) {
|
||||
said = append(said, subject+" "+id+" "+string(raw))
|
||||
return nil
|
||||
}}
|
||||
w, err := views.NewWriter(views.WriterConfig[Finding, FindingCleared, FindingsStated, FindingsAsked]{
|
||||
Module: Module, Node: "novox", State: findingState{store}, Observe: c.Observe, Logf: c.Logf, Now: c.Now,
|
||||
Publish: func(_ context.Context, subject string, _ []byte, _ string) error {
|
||||
store.events = append(store.events, subject)
|
||||
return nil
|
||||
},
|
||||
Listen: func(context.Context, string, func([]byte)) (func(), error) { return func() {}, nil }})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Writer = w
|
||||
return c, &said
|
||||
}
|
||||
|
||||
@@ -115,37 +144,36 @@ func checker(store *memStore, registry ...Probe) (*Checker, *[]string) {
|
||||
// could not run clears nothing and is written as its own finding at the second run in a row; a finding one look can
|
||||
// be wrong about waits for the second run; a deferred probe is never a pass (ADR 0564 §2, §6; issue 1952 SC1).
|
||||
func TestARunWritesFindingsAsTheyChange(t *testing.T) {
|
||||
var found []meshconditionsclient.Observation
|
||||
var found []Finding
|
||||
var fail error
|
||||
steady := Probe{ID: "P1", Kind: "stream-wrong", Run: func(context.Context, *Checker) ([]meshconditionsclient.Observation, error) { return found, fail }}
|
||||
steady := Probe{ID: "P1", Kind: "stream-wrong", Run: func(context.Context, *Checker) ([]Finding, error) { return found, fail }}
|
||||
deferred := Probe{ID: "P2", Kind: "resolver-wrong", Deferred: "the overlay's addresses"}
|
||||
missing := meshconditionsclient.Observation{Ref: "stream-missing", Severity: meshconditionsclient.Urgent, Scope: meshconditionsclient.ScopeBus, ID: "EVENTS",
|
||||
Summary: "the stream EVENTS is not on the bus"}
|
||||
behind := meshconditionsclient.Observation{Kind: kindDataUnmeasured, Ref: "behind", Severity: meshconditionsclient.Warning,
|
||||
Scope: meshconditionsclient.ScopeBus, ID: "EVENTS.controller", Summary: "far behind"}
|
||||
missing := finding(meshconditionsclient.ScopeBus, "EVENTS", "stream-missing", "", Finding{Severity: meshconditionsclient.Urgent,
|
||||
Summary: "the stream EVENTS is not on the bus", Evidence: said("no stream EVENTS")})
|
||||
behind := finding(meshconditionsclient.ScopeBus, "EVENTS.controller", "", "", Finding{Kind: kindDataUnmeasured, Severity: meshconditionsclient.Warning,
|
||||
Summary: "far behind"})
|
||||
store := newStore()
|
||||
c, said := checker(store, steady, deferred)
|
||||
|
||||
steps := []struct {
|
||||
name string
|
||||
found []meshconditionsclient.Observation
|
||||
fail error
|
||||
want []string // finding keys written after the run
|
||||
verdict string
|
||||
missingPs int // puts of the missing finding so far
|
||||
name string
|
||||
found []Finding
|
||||
fail error
|
||||
want []string // finding keys written after the run
|
||||
verdict string
|
||||
}{
|
||||
{"a finding is written", []meshconditionsclient.Observation{missing}, nil, []string{"bus.EVENTS.stream-missing"}, VerdictFail, 1},
|
||||
{"the same again is no write", []meshconditionsclient.Observation{missing}, nil, []string{"bus.EVENTS.stream-missing"}, VerdictFail, 1},
|
||||
{"one look can be wrong: held", []meshconditionsclient.Observation{missing, behind}, nil, []string{"bus.EVENTS.stream-missing"}, VerdictFail, 1},
|
||||
{"seen twice: written", []meshconditionsclient.Observation{missing, behind}, nil,
|
||||
[]string{"bus.EVENTS.controller.behind", "bus.EVENTS.stream-missing"}, VerdictFail, 1},
|
||||
{"a finding is written", []Finding{missing}, nil, []string{"bus.EVENTS.stream-missing.stream-wrong"}, VerdictFail},
|
||||
{"the same again is stated once more", []Finding{missing}, nil, []string{"bus.EVENTS.stream-missing.stream-wrong"}, VerdictFail},
|
||||
{"one look can be wrong: held", []Finding{missing, behind}, nil, []string{"bus.EVENTS.stream-missing.stream-wrong"}, VerdictFail},
|
||||
{"seen twice: written", []Finding{missing, behind}, nil,
|
||||
[]string{"bus.EVENTS.stream-missing.stream-wrong", "bus.EVENTS=2econtroller." + kindDataUnmeasured}, VerdictFail},
|
||||
{"blind once: clears nothing, held", nil, errors.New("timed out"),
|
||||
[]string{"bus.EVENTS.controller.behind", "bus.EVENTS.stream-missing"}, VerdictFailedToRun, 1},
|
||||
[]string{"bus.EVENTS.stream-missing.stream-wrong", "bus.EVENTS=2econtroller." + kindDataUnmeasured}, VerdictFailedToRun},
|
||||
{"blind twice: its own finding", nil, errors.New("timed out"),
|
||||
[]string{"bus.EVENTS.controller.behind", "bus.EVENTS.stream-missing", "probe.P1.failed"}, VerdictFailedToRun, 1},
|
||||
{"no longer seen: cleared", nil, nil, nil, VerdictPass, 1},
|
||||
[]string{"bus.EVENTS.stream-missing.stream-wrong", "bus.EVENTS=2econtroller." + kindDataUnmeasured, "probe.P1.probe-failed"}, VerdictFailedToRun},
|
||||
{"no longer seen: cleared", nil, nil, nil, VerdictPass},
|
||||
}
|
||||
for _, s := range steps {
|
||||
for i, s := range steps {
|
||||
found, fail = s.found, s.fail
|
||||
run := c.RunOnce(context.Background(), "the test")
|
||||
if got := store.findingKeys(); !slices.Equal(got, s.want) {
|
||||
@@ -157,11 +185,12 @@ func TestARunWritesFindingsAsTheyChange(t *testing.T) {
|
||||
if run.Probes[1].Verdict != VerdictDeferred || !strings.Contains(run.Probes[1].Error, "overlay") {
|
||||
t.Errorf("%s: a deferred probe answered %+v", s.name, run.Probes[1])
|
||||
}
|
||||
if n := store.puts[FindingsState+"/bus.EVENTS.stream-missing"]; n != s.missingPs {
|
||||
t.Errorf("%s: the unchanged finding was written %d times", s.name, n)
|
||||
// The writer says one `findings-stated` a run, on the subject it forms itself (design 47 G A-13).
|
||||
if n := len(store.events); n != i+1 || store.events[i] != "mesh.stream.mesh-self-check.event.findings-stated.novox" {
|
||||
t.Errorf("%s: the writer said %v", s.name, store.events)
|
||||
}
|
||||
}
|
||||
raw, _, _ := store.Get(FindingsState, "probe.P1.failed")
|
||||
raw, _, _ := store.Get(FindingsState, "probe.P1.probe-failed")
|
||||
if raw != nil {
|
||||
t.Errorf("the blind finding outlived the probe running again: %s", raw)
|
||||
}
|
||||
@@ -189,12 +218,12 @@ func TestARunWritesFindingsAsTheyChange(t *testing.T) {
|
||||
t.Errorf("the last verdict counts %+v", r.Counts)
|
||||
}
|
||||
// A finding carries the operator's words of its kind, the probe that found it and who observed it.
|
||||
found, fail = []meshconditionsclient.Observation{missing}, nil
|
||||
found, fail = []Finding{missing}, nil
|
||||
c.RunOnce(context.Background(), "the test")
|
||||
raw, _, _ = store.Get(FindingsState, "bus.EVENTS.stream-missing")
|
||||
raw, _, _ = store.Get(FindingsState, "bus.EVENTS.stream-missing.stream-wrong")
|
||||
f, err := decodeFinding(raw)
|
||||
if err != nil || f.Kind != "stream-wrong" || f.Source != "P1" || f.Observer != Module || f.Headline == "" ||
|
||||
f.Explanation == "" || f.Ref != "stream-missing" || len(f.Evidence) != 1 || f.Run == "" {
|
||||
f.Explanation == "" || f.ID != "EVENTS.stream-missing" || f.Subject.ID != f.ID || f.Subject.Scope != f.Scope || len(f.Evidence) != 1 || f.Run == "" || f.Since.IsZero() {
|
||||
t.Errorf("the written finding is %+v", f)
|
||||
}
|
||||
// Blind to its own bucket, a run writes and clears nothing.
|
||||
@@ -202,7 +231,7 @@ func TestARunWritesFindingsAsTheyChange(t *testing.T) {
|
||||
found = nil
|
||||
c.RunOnce(context.Background(), "the test")
|
||||
store.keysErr = nil
|
||||
if got := store.findingKeys(); !slices.Equal(got, []string{"bus.EVENTS.stream-missing"}) {
|
||||
if got := store.findingKeys(); !slices.Equal(got, []string{"bus.EVENTS.stream-missing.stream-wrong"}) {
|
||||
t.Errorf("a run blind to its findings changed them: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -212,9 +241,9 @@ func TestAProbeThatPanicsOrOverrunsCouldNotRun(t *testing.T) {
|
||||
within := Within
|
||||
Within = 50 * time.Millisecond
|
||||
t.Cleanup(func() { Within = within })
|
||||
for name, run := range map[string]func(context.Context, *Checker) ([]meshconditionsclient.Observation, error){
|
||||
"panic": func(context.Context, *Checker) ([]meshconditionsclient.Observation, error) { panic("boom") },
|
||||
"overrun": func(ctx context.Context, _ *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
for name, run := range map[string]func(context.Context, *Checker) ([]Finding, error){
|
||||
"panic": func(context.Context, *Checker) ([]Finding, error) { panic("boom") },
|
||||
"overrun": func(ctx context.Context, _ *Checker) ([]Finding, error) {
|
||||
time.Sleep(time.Second)
|
||||
return nil, nil
|
||||
},
|
||||
@@ -232,12 +261,12 @@ func TestAProbeThatPanicsOrOverrunsCouldNotRun(t *testing.T) {
|
||||
// every state the module keeps (ADR 0564 §12).
|
||||
func TestTheVerbsAndReads(t *testing.T) {
|
||||
store := newStore()
|
||||
many := make([]meshconditionsclient.Observation, 25)
|
||||
many := make([]Finding, 25)
|
||||
for i := range many {
|
||||
many[i] = meshconditionsclient.Observation{Severity: meshconditionsclient.Warning, Scope: meshconditionsclient.ScopeBus, ID: fmt.Sprintf("S%02d", i), Summary: "x"}
|
||||
many[i] = finding(meshconditionsclient.ScopeBus, fmt.Sprintf("S%02d", i), "", "", Finding{Severity: meshconditionsclient.Warning, Summary: "x"})
|
||||
}
|
||||
c, _ := checker(store, Probe{ID: "D7", Kind: "stream-wrong",
|
||||
Run: func(context.Context, *Checker) ([]meshconditionsclient.Observation, error) { return many, nil }})
|
||||
Run: func(context.Context, *Checker) ([]Finding, error) { return many, nil }})
|
||||
if _, err := c.Doctor(false, "", "x"); err == nil || !strings.Contains(err.Error(), "first run") {
|
||||
t.Fatalf("before a run: %v", err)
|
||||
}
|
||||
@@ -283,11 +312,11 @@ func TestTheVerbsAndReads(t *testing.T) {
|
||||
// and is reported by the verb as off by setting; the default list is D13 and D-fill (the operator, 2026-10-30).
|
||||
func TestOnlyTheProbesTheSettingListsRun(t *testing.T) {
|
||||
ran := map[string]int{}
|
||||
finds := meshconditionsclient.Observation{Ref: "stream-missing", Severity: meshconditionsclient.Urgent, Scope: meshconditionsclient.ScopeBus, ID: "EVENTS", Summary: "gone"}
|
||||
finds := finding(meshconditionsclient.ScopeBus, "EVENTS", "stream-missing", "", Finding{Severity: meshconditionsclient.Urgent, Summary: "gone"})
|
||||
probe := func(id string) Probe {
|
||||
return Probe{ID: id, Kind: "stream-wrong", Run: func(context.Context, *Checker) ([]meshconditionsclient.Observation, error) {
|
||||
return Probe{ID: id, Kind: "stream-wrong", Run: func(context.Context, *Checker) ([]Finding, error) {
|
||||
ran[id]++
|
||||
return []meshconditionsclient.Observation{finds}, nil
|
||||
return []Finding{finds}, nil
|
||||
}}
|
||||
}
|
||||
store := newStore()
|
||||
@@ -325,8 +354,8 @@ func TestOnlyTheProbesTheSettingListsRun(t *testing.T) {
|
||||
}
|
||||
|
||||
// decodeFinding reads a finding as the seat mesh-conditions lays it out.
|
||||
func decodeFinding(raw []byte) (meshconditions.FindingsByKeyValue, error) {
|
||||
var f meshconditions.FindingsByKeyValue
|
||||
func decodeFinding(raw []byte) (Finding, error) {
|
||||
var f Finding
|
||||
err := json.Unmarshal(raw, &f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
@@ -157,6 +157,9 @@ func (p *player) handle(m wire) {
|
||||
err = kv.Delete(p.ctx, a.Key)
|
||||
}
|
||||
p.answer(m.ID, map[string]any{}, err)
|
||||
case "mesh/subscribe":
|
||||
// The finding writer's consumer of the asks for findings (design 47 G A-16); no ask comes in this test.
|
||||
p.answer(m.ID, map[string]any{}, nil)
|
||||
case "mesh/seat.publish":
|
||||
var e struct {
|
||||
Subject string `json:"subject"`
|
||||
@@ -252,6 +255,10 @@ func TestTheSelfCheckWholeRunsOnTheBus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
statements, err := nc.SubscribeSync("mesh.mod." + Module + ".event.findings-stated")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, bin)
|
||||
cmd.Env = append(os.Environ(), "MESH_NODE=ace", "MESH_MODULE="+Module)
|
||||
stdin, _ := cmd.StdinPipe()
|
||||
@@ -282,16 +289,26 @@ func TestTheSelfCheckWholeRunsOnTheBus(t *testing.T) {
|
||||
verdicts["D2"] != VerdictDeferred || answer.Run.Controller != "ace" || !strings.Contains(answer.Run.Why, "person.jochen") {
|
||||
t.Errorf("the run answered %+v", answer.Run)
|
||||
}
|
||||
msg, err := checked.NextMsg(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("checked was not said: %v", err)
|
||||
}
|
||||
// The writer's statement at the start says checked too: the run of the person's ask is the one this waits for.
|
||||
var said Run
|
||||
if err := json.Unmarshal(msg.Data, &said); err != nil || said.Run != answer.Run.Run {
|
||||
t.Errorf("checked said %s", msg.Data)
|
||||
for said.Run != answer.Run.Run {
|
||||
msg, err := checked.NextMsg(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("checked of %s was not said: %v", answer.Run.Run, err)
|
||||
}
|
||||
if err := json.Unmarshal(msg.Data, &said); err != nil {
|
||||
t.Fatalf("checked said %s", msg.Data)
|
||||
}
|
||||
}
|
||||
for stated := ""; !strings.Contains(stated, `"id":"EVENTS.stream-missing"`); {
|
||||
msg, err := statements.NextMsg(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("the writer did not say findings-stated with the missing stream (last %s): %v", stated, err)
|
||||
}
|
||||
stated = string(msg.Data)
|
||||
}
|
||||
findings, _ := js.KeyValue(ctx, Module+"_"+FindingsState)
|
||||
e, err := findings.Get(ctx, "bus.EVENTS.stream-missing")
|
||||
e, err := findings.Get(ctx, "bus.EVENTS.stream-missing.stream-wrong")
|
||||
if err != nil {
|
||||
t.Fatalf("the missing stream is not a finding on the bus: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package main
|
||||
|
||||
// The operator's words for the kinds the self-check raises; the observation and its finding are the conditions
|
||||
// client's (novox/hq ADR 0564 §2, ADR 0634 §4).
|
||||
|
||||
import "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
// The operator's words for the kinds the self-check raises, written into a finding's headline and explanation
|
||||
// (novox/hq ADR 0564 §2; design 47 G §689).
|
||||
|
||||
// worded gives a finding the operator's words registered for its kind, where it says none of its own.
|
||||
func worded(f meshconditionsclient.Observation) meshconditionsclient.Observation {
|
||||
func worded(f Finding) Finding {
|
||||
w, ok := words[f.Kind]
|
||||
if !ok {
|
||||
return f
|
||||
@@ -17,46 +15,38 @@ func worded(f meshconditionsclient.Observation) meshconditionsclient.Observation
|
||||
if f.Explanation == "" {
|
||||
f.Explanation = w.Explanation
|
||||
}
|
||||
if f.Resolved == "" {
|
||||
f.Resolved = w.Resolved
|
||||
}
|
||||
if f.Needs == "" {
|
||||
f.Needs = w.Needs
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type plain struct{ Headline, Explanation, Resolved, Needs string }
|
||||
type plain struct{ Headline, Explanation, Needs string }
|
||||
|
||||
// words are the controller's words for each kind the self-check raises (mesh-controller plain_words.go), the
|
||||
// controller's name for itself replaced by the self-check's where the words spoke of it.
|
||||
var words = map[string]plain{
|
||||
"stream-wrong": {Headline: "Part of the bus's storage is wrong",
|
||||
Needs: "check the machine the bus runs on; the details say what is missing.",
|
||||
Explanation: "A store the mesh keeps on the bus is missing or not as defined, so what it keeps there may be lost.",
|
||||
Resolved: "The bus's storage is right again"},
|
||||
Explanation: "A store the mesh keeps on the bus is missing or not as defined, so what it keeps there may be lost."},
|
||||
"bus-reconnects": {Headline: "A client keeps losing the bus",
|
||||
Explanation: "One of the mesh's clients lost its connection to the bus again and again in the last hour. While " +
|
||||
"it reconnects, what it says and what it is asked waits.",
|
||||
Resolved: "Resolved: the client stays connected"},
|
||||
"it reconnects, what it says and what it is asked waits."},
|
||||
"core-unhealthy": {Headline: "The bus is not healthy",
|
||||
Needs: "check the machine the bus runs on; the details say what fails.",
|
||||
Explanation: "The bus, which carries every message in the mesh, is not healthy. Instructions, builds and tool " +
|
||||
"calls may fail until it recovers.",
|
||||
Resolved: "The bus is healthy again"},
|
||||
"calls may fail until it recovers."},
|
||||
"watchdogs-silent": {Headline: "The mesh's watchdogs stopped",
|
||||
Needs: "restart the watchman if this stays, from the mesh MCP server.",
|
||||
Explanation: "The watchdogs, which notice when something expected does not happen, do not answer, so missed " +
|
||||
"signals are not noticed.",
|
||||
Resolved: "The watchdogs run again"},
|
||||
"signals are not noticed."},
|
||||
"probe-failed": {Headline: "A check of the mesh could not run",
|
||||
Explanation: "One of the mesh's own checks could not run, so what it watches is unknown for now. It runs again " +
|
||||
"every few minutes.",
|
||||
Resolved: "The check runs again"},
|
||||
"every few minutes."},
|
||||
}
|
||||
|
||||
// confirm says one look can be wrong about the finding: it is written only when two runs in a row see it (issue 277).
|
||||
// The kinds are the self-check's own: a probe that could not run, and a holder that measured nothing of the data.
|
||||
func confirm(o meshconditionsclient.Observation) bool {
|
||||
func confirm(o Finding) bool {
|
||||
return o.Kind == "probe-failed" || o.Kind == kindDataUnmeasured
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user