From 0fa00d5e6c945f1e1e11fa4b5a7a6c3c52237cea Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:02:01 +0100 Subject: [PATCH] =?UTF-8?q?The=20watchman=20raises=20what=20a=20node=20kee?= =?UTF-8?q?ps=20of=20a=20module=20as=20it=20runs,=20for=20a=20manifest=20t?= =?UTF-8?q?hat=20does=20not=20yet=20say=20it,=20from=20the=20module's=20ha?= =?UTF-8?q?ve,=20and=20clears=20it=20once=20the=20have=20says=20none=20(AD?= =?UTF-8?q?R=200601=20=C2=A74;=20review=20round=201,=20finding=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/watch/findings_test.go | 23 ++++++++++++++ .../internal/watch/signals.go | 30 +++++++++++++++++++ .../internal/watch/watch.go | 8 +++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 3e94a361..57f26142 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -72,6 +72,29 @@ func TestTheWatchmansFindings(t *testing.T) { } w.ModuleVerdict("nats", value(""), Put) }, map[string]string{}}, + {"ADR 0601 §4: what a node keeps of a module as it runs, for a manifest that does not say it yet, is a warning until the have says none", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { + have := func(kept ...string) []byte { + b, err := delivery.EncodeHave(delivery.Have{FP: "src2:" + strings.Repeat("a", 64), Phase: delivery.PhaseRunning, Health: delivery.HealthOK, Kept: kept}) + if err != nil { + t.Fatal(err) + } + return b + } + k := FindingKey(ScopeModule, "runner-x.ace", "kept") + w.Have("ace.runner-x", have("runner-x runs the tool runner's process here"), Put) + f := o.findings[k] + if f.Kind != KindKeptAsRuns || f.Severity != Warning || !strings.Contains(f.Summary, "the tool runner's process") { + t.Fatalf("findings = %v", o.findings) + } + c.t = c.t.Add(time.Minute) + w.Have("ace.runner-x", have("runner-x runs the tool runner's process here"), Put) + if !o.findings[k].Since.Equal(f.Since) { + t.Errorf("a have written again moved the finding's since: %v, was %v", o.findings[k].Since, f.Since) + } + w.Have("ace.prov-y", have("prov-y carries its own secrets sealed"), Put) + w.Have("ace.runner-x", have(), Put) // its manifest says tool-runner now + w.Have("ace.prov-y", nil, Delete) + }, map[string]string{}}, {"WM4: a stuck key not moved is its finding, and its move deletes it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.Step("go", "n1.ask1", runningStep("ask1", t0), Put) expire(w, c, pulseKeyOf(t, checksKey, "ask1"), 120*time.Second) diff --git a/modules/mesh-delivery-watchman/internal/watch/signals.go b/modules/mesh-delivery-watchman/internal/watch/signals.go index e5bc2346..ee44118a 100644 --- a/modules/mesh-delivery-watchman/internal/watch/signals.go +++ b/modules/mesh-delivery-watchman/internal/watch/signals.go @@ -199,6 +199,7 @@ const ( KindUnhealthy = "module-unhealthy" KindNeedsOperator = "needs-operator" KindDeadLetters = "max-deliveries" + KindKeptAsRuns = "kept-as-it-runs" ) func stamp(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 MST") } @@ -476,11 +477,13 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav w.unsignal("unhealthy-confirm", module+"."+node) w.unsignal("unhealthy", module+"."+node) w.unfind(unhealthyKey) + w.keptFinding(key, node, module, nil) w.applyCheck(node) return } w.haveState[key] = *h now := w.now() + w.keptFinding(key, node, module, h.Kept) // S21: a have refused or applied in part says phase failed, with the node's words in its detail. if h.Phase == delivery.PhaseFailed { @@ -531,6 +534,33 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav } } +// keptFinding raises, as a warning, what a node keeps of a module as it runs because its manifest does not yet say what shared +// code no longer infers (its have's Kept, novox/hq ADR 0601 §4: the tool runner, the core's credentials), naming each; and +// clears it once the have says none, or is gone. Its since is the first have that said it, so a have written again moves +// nothing. +func (w *Watchman) keptFinding(key, node, module string, kept []string) { + id := module + "." + node + fkey := FindingKey(ScopeModule, id, "kept") + if len(kept) == 0 { + delete(w.keptSince, key) + w.unfind(fkey) + return + } + if w.keptSince == nil { + w.keptSince = map[string]time.Time{} + } + since, ok := w.keptSince[key] + if !ok { + since = w.now() + w.keptSince[key] = since + } + words := strings.Join(kept, "; ") + w.find(Finding{Scope: ScopeModule, ID: id, Token: "kept", Kind: KindKeptAsRuns, Machine: node, Severity: Warning, + Source: "node-engine", Since: since, Summary: fmt.Sprintf("%s on %s is kept as it runs: %s", module, node, words), + Said: firstLine(words), Headline: fmt.Sprintf("%s kept as it runs on %s", module, node), + Needs: "a build of " + module + " whose manifest says it (tool-runner, core-credentials)"}) +} + // ControlNodeModule is the module whose have names a control node: the bus module, so the control node is the bus node, // the one stably reachable node every part depends on (novox/hq ADR 0590, issue 1126: the controller that named it ended). // The same name the bus module's events are heard under (SlowConsumerSubject). diff --git a/modules/mesh-delivery-watchman/internal/watch/watch.go b/modules/mesh-delivery-watchman/internal/watch/watch.go index fa6d5b7d..3c769493 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch.go @@ -60,9 +60,11 @@ type Watchman struct { presence map[string]presence pulses map[string]*pulse // live pulses, by pulse key stuck map[string]delivery.Stuck - wants map[string]string // the fingerprint of each want that stands, by want key - haves map[string]string // the fingerprint each node runs of a module, by have key - bound time.Duration + // keptSince is, by have key, when its have first said what the node keeps of the module as it runs (keptFinding). + keptSince map[string]time.Time + wants map[string]string // the fingerprint of each want that stands, by want key + haves map[string]string // the fingerprint each node runs of a module, by have key + bound time.Duration // findings is every finding written, by key, as written; seededFindings those read at the start and not yet observed // again. findings map[string][]byte