Raise a refused manifest as the watchman's finding, so the operator hears it once (ADR 0578 §1)
Issue 1585, S10: the watchman watches the indexer's modules and finds manifest-refused for a module whose value carries a refusal, ending it once accepted. Its mesh-sdk pin moves to the group head, so it takes health.Finding's Ref (issue 1952 renamed Token). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -345,6 +345,17 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
go func() { _ = follow(ctx, hw, healthEach) }()
|
||||
})
|
||||
}
|
||||
// The indexer's modules (ADR 0578 §1): a module whose value carries a refusal of its manifest is a finding; read
|
||||
// before the findings are reconciled, each change heard on the watch.
|
||||
verdictEach := func(e jetstream.KeyValueEntry) { w.ModuleVerdict(e.Key(), e.Value(), opOf(e)) }
|
||||
var verdictWatch jetstream.KeyWatcher
|
||||
if kv, err := js.KeyValue(ctx, delivery.ModulesBucket); err == nil {
|
||||
if verdictWatch, err = settle(ctx, kv, verdictEach); err != nil {
|
||||
process.NotWatched(logger.Printf, delivery.ModulesBucket, err, "no refused manifest is said until the watchman starts again")
|
||||
}
|
||||
} else {
|
||||
process.NotWatched(logger.Printf, delivery.ModulesBucket, err, "no refused manifest is said until the watchman starts again")
|
||||
}
|
||||
dead := &deadKeeper{ctx: ctx, js: js, w: w, logf: logger.Printf}
|
||||
w.SetRecount(dead.count)
|
||||
dead.count()
|
||||
@@ -360,6 +371,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
if healthWatch != nil {
|
||||
go func() { errs <- follow(ctx, healthWatch, healthEach) }()
|
||||
}
|
||||
if verdictWatch != nil {
|
||||
go func() { errs <- follow(ctx, verdictWatch, verdictEach) }()
|
||||
}
|
||||
if provWatch != nil {
|
||||
go func() {
|
||||
errs <- follow(ctx, provWatch, func(e jetstream.KeyValueEntry) { w.Provision(e.Key(), e.Value(), opOf(e)) })
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-watchman
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2 h1:RF73nVI1dTrrmlFosVDTMJmpUQ+rEeB7++BHH/RvZFg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -67,7 +67,7 @@ func EncodeFinding(f Finding) ([]byte, error) {
|
||||
if said == "" {
|
||||
said = f.Summary
|
||||
}
|
||||
return health.EncodeFinding(health.Finding{Key: f.Key(), Kind: f.Kind, Token: f.Token,
|
||||
return health.EncodeFinding(health.Finding{Key: f.Key(), Kind: f.Kind, Ref: 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}},
|
||||
@@ -80,7 +80,7 @@ func DecodeFinding(data []byte) (Finding, error) {
|
||||
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,
|
||||
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: 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 {
|
||||
|
||||
@@ -33,6 +33,24 @@ func TestTheWatchmansFindings(t *testing.T) {
|
||||
// want are the finding keys standing at the end, with their severity.
|
||||
want map[string]string
|
||||
}{
|
||||
{"ADR 0578 §1: a module whose value carries a refusal of its manifest is a finding until it is accepted", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
|
||||
value := func(verdict string, problems ...string) []byte {
|
||||
b, err := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64),
|
||||
Repository: "novox/mesh-catalog", Commit: "c", Verdict: verdict, Problems: problems})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
w.ModuleVerdict("nats", value(delivery.VerdictRefused, "removes a stream"), Put)
|
||||
w.ModuleVerdict("power", value(delivery.VerdictRefused, "shows nothing"), Put)
|
||||
k := FindingKey(ScopeModule, "nats", KindManifestRefused)
|
||||
if f := o.findings[k]; !strings.Contains(f.Summary, "refused the manifest of nats: removes a stream") {
|
||||
t.Fatalf("findings = %v", o.findings)
|
||||
}
|
||||
w.ModuleVerdict("nats", value(""), Put)
|
||||
w.ModuleVerdict("power", 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)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// KindManifestRefused is a module whose manifest the indexer's judge refused (novox/hq ADR 0578 §1): every reader holds
|
||||
// it, composing nothing of its own, until a mended manifest is merged.
|
||||
const KindManifestRefused = "manifest-refused"
|
||||
|
||||
// ModuleVerdict is a change of the indexer's value of a module (delivery.ModulesBucket): a value carrying a refusal is a
|
||||
// finding, one that does not (accepted, or not judged yet) or a deleted key ends it.
|
||||
func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
key := FindingKey(ScopeModule, module, KindManifestRefused)
|
||||
if op != Put {
|
||||
w.unfind(key)
|
||||
return
|
||||
}
|
||||
m, err := delivery.DecodeModule(value)
|
||||
if err != nil {
|
||||
w.say("the indexer's value of %s cannot be read, so whether its manifest is refused is left as it was: %v", module, err)
|
||||
return
|
||||
}
|
||||
v := m.VerdictOf(module)
|
||||
if refused, _ := delivery.TakeManifestVerdict(v); !refused {
|
||||
w.unfind(key)
|
||||
return
|
||||
}
|
||||
w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestRefused, Severity: Warning, Source: delivery.ModulesBucket,
|
||||
Summary: v.Why(),
|
||||
Said: strings.Join(v.Problems, "; "),
|
||||
Headline: "The manifest of " + module + " is refused",
|
||||
Explanation: "The indexer's judge refused it, so every node, the grants part and the assigner hold " + module + ": " +
|
||||
"nothing of it changes until a mended manifest is merged, and every other module goes on.",
|
||||
Needs: "a pull request that mends the manifest of " + module})
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
|
||||
reads := slices.Clone(m.Reads)
|
||||
slices.Sort(reads)
|
||||
if !reflect.DeepEqual(reads, []string{"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-controller.lease",
|
||||
"mesh-delivery-assigner.wants", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"}) {
|
||||
"mesh-delivery-assigner.wants", "mesh-delivery-indexer.modules", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"}) {
|
||||
t.Errorf("reads = %v", reads)
|
||||
}
|
||||
// 1952/WM1: the watchman holds the seat mesh-watchdogs at mesh scope, its first seat (novox/hq ADR 0564 §1, §4).
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"mesh-build-node.steps",
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-host.health",
|
||||
"mesh-provisioner.provisions",
|
||||
"node-engine.have"
|
||||
|
||||
Reference in New Issue
Block a user