Files
mesh-delivery/modules/mesh-delivery-watchman/internal/watch/finding.go
T
jschoubben da787ae360 Watchman: hold mesh-watchdogs and write its watchdogs as findings, so health can leave the controller (issue 1952 WM1-WM5)
The signals rows S1, S8, S9, S10, S11, S21 and the haves' unhealthy and needs-operator are pulses whose expiry is a finding;
stuck keys and presence are its own findings; it keeps the dead letters from the lease write on its own consumer, lists and
shows them, and refuses deliver and drop until the hand-act log's home (issue 1953). Tools read every state it keeps and
start resync. Pins mesh-sdk 2a04bf0.
2026-10-29 21:44:18 +01:00

160 lines
5.5 KiB
Go

package watch
import (
"time"
"git.novox.be/novox/mesh-sdk/go/health"
)
// The watchman is an observer of the seat mesh-watchdogs (novox/hq ADR 0564 §2, §4): what it observes wrong it writes as a
// finding, one key `<scope>.<id>.<kind>` of its own bucket `findings` in mesh-sdk's layout (health.Finding), only when what
// it says changes, and it deletes the key when it no longer observes it. The condition keeper (mesh-conditions) raises and
// clears the conditions from them; the watchman writes no condition and reads none (Limits 11).
// FindingsBucket is the watchman's findings.
var FindingsBucket = health.FindingsBucket(health.WatchmanModule)
// Observer is the name a finding of the watchman carries.
const Observer = health.WatchmanModule
// The scopes and severities a finding of the watchman names, as the controller's conditions name them, so the keeper's
// `compare` meets the same keys.
const (
ScopeMachine = health.ScopeMachine
ScopeDelivery = health.ScopeDelivery
ScopeModule = health.ScopeModule
ScopeProvider = health.ScopeProvider
ScopeBus = health.ScopeBus
ScopeCore = health.ScopeCore
Urgent = string(health.Urgent)
Warning = string(health.Warning)
)
// Finding is what the watchman builds of one thing it observes wrong: its subject (scope, id, the machine and the others
// it concerns), kind and severity, its evidence (Summary, and Said with Since, its one observation), the words the
// operator reads (ADR 0253; empty leaves the keeper's wording of the kind), and the row or fact it comes from (Source).
// It is written as health.Finding.
type Finding struct {
Scope, ID, Token, Kind, Machine string
Also []string
Severity string
Summary, Said string
Headline, Explanation string
Needs, Resolved string
Source string
Observer string
// Since is when what it says began, as the watchman saw it: the finding's time and its observation's, so the same
// thing observed again writes the same bytes.
Since time.Time
}
// FindingKey composes a finding's key as the controller composes a condition's (health.Key).
func FindingKey(scope, id, token string) string { return health.FindingKey(scope, id, token) }
// Key is the finding's key: its token, or its kind when it has none.
func (f Finding) Key() string {
token := f.Token
if token == "" {
token = f.Kind
}
return FindingKey(f.Scope, f.ID, token)
}
// EncodeFinding is f in mesh-sdk's layout, refusing one the keeper could not make a condition of.
func EncodeFinding(f Finding) ([]byte, error) {
said := f.Said
if said == "" {
said = f.Summary
}
return health.EncodeFinding(health.Finding{Key: f.Key(), Kind: f.Kind, Token: f.Token,
Subject: health.Subject{Scope: f.Scope, ID: f.ID, Machine: f.Machine, Also: f.Also},
Severity: health.Severity(f.Severity), Summary: f.Summary, Headline: f.Headline, Explanation: f.Explanation,
Resolved: f.Resolved, Needs: f.Needs, Evidence: []health.Evidence{{At: f.Since.UTC(), Said: said}},
Source: f.Source, Observer: Observer, At: f.Since.UTC()})
}
// DecodeFinding reads a finding the watchman wrote.
func DecodeFinding(data []byte) (Finding, error) {
h, err := health.DecodeFinding(data)
if err != nil {
return Finding{}, err
}
f := Finding{Scope: h.Subject.Scope, ID: h.Subject.ID, Token: h.Token, Kind: h.Kind, Machine: h.Subject.Machine,
Also: h.Subject.Also, Severity: string(h.Severity), Summary: h.Summary, Headline: h.Headline, Explanation: h.Explanation,
Needs: h.Needs, Resolved: h.Resolved, Source: h.Source, Observer: h.Observer, Since: h.At}
if len(h.Evidence) > 0 {
f.Said = h.Evidence[0].Said
}
return f, nil
}
// find writes a finding when what it says differs from what the bucket holds (ADR 0289 §1).
func (w *Watchman) find(f Finding) {
value, err := EncodeFinding(f)
if err != nil {
w.say("no finding: %v", err)
return
}
key := f.Key()
delete(w.seededFindings, key)
if string(w.findings[key]) == string(value) {
return
}
if err := w.out.PutFinding(key, value); err != nil {
w.say("cannot write the finding %s: %v", key, err)
return
}
w.findings[key] = value
}
// unfind deletes a finding the watchman no longer observes.
func (w *Watchman) unfind(key string) {
if _, ok := w.findings[key]; !ok {
return
}
if err := w.out.DeleteFinding(key); err != nil {
w.say("cannot delete the finding %s: %v", key, err)
return
}
delete(w.findings, key)
}
// SeedFinding tells the watchman a finding that stands in its bucket at its start; ReconcileFindings deletes it unless what
// the watchman reads at its start observes it again.
func (w *Watchman) SeedFinding(key string, value []byte) {
w.mu.Lock()
defer w.mu.Unlock()
w.findings[key] = value
w.seededFindings[key] = true
}
// Findings is every finding the watchman holds now, by key.
func (w *Watchman) Findings() map[string]Finding {
w.mu.Lock()
defer w.mu.Unlock()
out := map[string]Finding{}
for k, v := range w.findings {
if f, err := DecodeFinding(v); err == nil {
out[k] = f
}
}
return out
}
// Rewrite writes every finding the watchman holds again (resync, ADR 0564 §12): a bucket cleared or a key lost by hand is
// written back as the watchman observes it. It answers how many it wrote.
func (w *Watchman) Rewrite() int {
w.mu.Lock()
defer w.mu.Unlock()
n := 0
for key, value := range w.findings {
if err := w.out.PutFinding(key, value); err != nil {
w.say("cannot write the finding %s again: %v", key, err)
continue
}
n++
}
return n
}