Files
mesh-delivery/modules/mesh-delivery-watchman/internal/watch/finding.go
T

175 lines
6.4 KiB
Go

package watch
import (
"time"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
)
// 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 the seat mesh-conditions' layout (meshconditionsclient), 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 = mcc.FindingsBucket(Observer)
// Observer is the name a finding of the watchman carries.
const Observer = "mesh-delivery-watchman"
// 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 = mcc.ScopeMachine
ScopeDelivery = mcc.ScopeDelivery
ScopeModule = mcc.ScopeModule
ScopeProvider = mcc.ScopeProvider
ScopeBus = mcc.ScopeBus
ScopeCore = mcc.ScopeCore
Urgent = mcc.Urgent
Warning = mcc.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 the seat's 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 (mcc.Key).
func FindingKey(scope, id, token string) string { return mcc.Key(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 the seat's layout, refusing one the keeper could not make a condition of.
func EncodeFinding(f Finding) ([]byte, error) {
return mcc.EncodeFinding(mcc.Observation{Scope: f.Scope, ID: f.ID, Kind: f.Kind, Ref: f.Token, Machine: f.Machine, Also: f.Also,
Severity: f.Severity, Summary: f.Summary, Said: f.Said, Headline: f.Headline, Explanation: f.Explanation,
Resolved: f.Resolved, Needs: f.Needs, Source: f.Source}.Finding(Observer, "", f.Since))
}
// DecodeFinding reads a finding the watchman wrote.
func DecodeFinding(data []byte) (Finding, error) {
h, err := mcc.DecodeFinding(data)
if err != nil {
return Finding{}, err
}
f := Finding{Scope: h.Subject.Scope, ID: h.Subject.ID, Token: h.Ref, Kind: h.Kind, Machine: h.Subject.Machine,
Also: h.Subject.Also, Severity: 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
}
// NotHeard is the watchman's finding of a family it does not hear (heard.Kind, ADR 0615 §5): a consumer not there yet, a
// hearing or a watch that could not start; consequence says what goes unheard. Heard ends it, once the consumer binds.
func (w *Watchman) NotHeard(family, consequence string, why error) {
w.mu.Lock()
defer w.mu.Unlock()
summary, headline, explanation := heard.Words(Observer, family, consequence, why)
w.say("%s", summary)
w.find(Finding{Scope: ScopeModule, ID: heard.ID(Observer, family), Token: heard.Kind, Kind: heard.Kind, Severity: Warning,
Source: Observer + "." + heard.Kind, Since: w.now().UTC().Truncate(time.Second), Summary: summary, Headline: headline,
Explanation: explanation, Resolved: "the watchman's consumer of " + family + " is bound"})
}
// Heard ends the finding that the watchman does not hear family.
func (w *Watchman) Heard(family string) {
w.mu.Lock()
defer w.mu.Unlock()
w.unfind(heard.Key(Observer, family))
}