Builds: design 47 G §689, A-14 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
212 lines
7.6 KiB
Go
212 lines
7.6 KiB
Go
package watch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
|
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
|
|
|
"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 state `findings` in the seat mesh-conditions' generated layout, written
|
|
// through mesh-sdk's finding writer (design 47 G §689), 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 = Observer + "_findings"
|
|
|
|
// 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 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)
|
|
}
|
|
|
|
// KeyID is an id as the parts of a key: each dot-separated part made safe for a bus key, as the key of a condition is
|
|
// (mcc.Key), so the finding's key keeps the shape its condition had.
|
|
func KeyID(id string) string {
|
|
parts := strings.Split(id, ".")
|
|
for i, p := range parts {
|
|
p = unsafeKey.ReplaceAllString(strings.TrimSpace(p), "_")
|
|
if p == "" {
|
|
p = "_"
|
|
}
|
|
parts[i] = p
|
|
}
|
|
return strings.Join(parts, ".")
|
|
}
|
|
|
|
// Contract is f as the seat mesh-conditions' generated finding (design 47 G §686, §689): a ref, where the finding has a
|
|
// token other than its kind, goes into the id as `<id>.<ref>` (A-14), and is kept in `ref` so the finding reads back as
|
|
// it was written. The resolved words are not a field of the seat (A-22), and its `at` is its `since`, so the same thing
|
|
// observed again writes the same bytes.
|
|
func (f Finding) Contract() condseat.Finding {
|
|
id, ref := KeyID(f.ID), ""
|
|
if f.Token != "" && f.Token != f.Kind {
|
|
ref = f.Token
|
|
id = id + "." + KeyID(ref)
|
|
}
|
|
v := condseat.Finding{Scope: f.Scope, ID: id, Kind: f.Kind, Ref: ref, Severity: f.Severity, Summary: f.Summary,
|
|
Headline: f.Headline, Explanation: f.Explanation, Needs: f.Needs, Source: f.Source, Observer: Observer,
|
|
At: f.Since.UTC(), Since: f.Since.UTC(),
|
|
Subject: condseat.Subject{Scope: f.Scope, ID: id, Machine: f.Machine, Also: f.Also}}
|
|
if f.Said != "" {
|
|
v.Evidence = []condseat.Evidence{{At: f.Since.UTC(), Said: f.Said}}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// EncodeFinding is f in the seat's generated layout.
|
|
func EncodeFinding(f Finding) ([]byte, error) { return json.Marshal(f.Contract()) }
|
|
|
|
// DecodeFinding reads a finding the watchman wrote.
|
|
func DecodeFinding(data []byte) (Finding, error) {
|
|
var h condseat.Finding
|
|
if err := json.Unmarshal(data, &h); err != nil {
|
|
return Finding{}, err
|
|
}
|
|
id := h.ID
|
|
if h.Ref != "" {
|
|
id = strings.TrimSuffix(id, "."+h.Ref)
|
|
}
|
|
f := Finding{Scope: h.Scope, ID: 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, Source: h.Source, Observer: h.Observer, Since: h.Since}
|
|
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})
|
|
}
|
|
|
|
// 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))
|
|
}
|