From c87fc4e6cf7a72c111dd58334cdce4058cebbd86 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 21:50:39 +0100 Subject: [PATCH] G findings4 checker: the checker's check-could-not-run and its views reader's findings go through mesh-sdk's finding writer, which observes the raised ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §689, §686 (A-3), §639, §384, A-13 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../mesh-delivery-checker/findingwriter.go | 82 +++++++++++++++++++ .../cmd/mesh-delivery-checker/main.go | 23 ++++-- modules/mesh-delivery-checker/go.mod | 3 +- .../internal/check/checker_test.go | 7 +- .../internal/check/couldnotrun.go | 20 +++++ 5 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 modules/mesh-delivery-checker/cmd/mesh-delivery-checker/findingwriter.go diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/findingwriter.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/findingwriter.go new file mode 100644 index 00000000..ee0eeb3e --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/findingwriter.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-sdk/go/keys" + "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/views" + + "mesh-delivery-checker/internal/check" +) + +type findingWriterOf = views.Writer[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent] + +// checkerFindings is the checker's finding writer (design 47 G §461, §689): it owns the finding check-could-not-run beside +// its views reader's, observes the raised ones of its kept checks, and writes both as the generated contract type. The +// writer forms its subjects itself (A-13). +type checkerFindings struct { + w *findingWriterOf + node string +} + +// newCheckerFindings makes and starts the writer; observe says the checks that could not run and whose finding is +// written (check.Checker.RaisedCouldNotRun). +func newCheckerFindings(ctx context.Context, cred process.Account, conn *nats.Conn, js jetstream.JetStream, state process.State, + observe func(context.Context) ([]views.Finding, error), logf func(string, ...any)) (*checkerFindings, error) { + c := &checkerFindings{node: cred.Node} + w, err := views.NewWriter(views.WriterConfig[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]{ + Module: cred.Module, State: state, Node: cred.Node, JS: js, Conn: conn, Logf: logf, + Kinds: []string{check.CouldNotRunKind}, + Observe: func(ctx context.Context) (views.Observed[condseat.Finding], error) { + found, err := observe(ctx) + if err != nil { + return views.Observed[condseat.Finding]{}, err + } + var out views.Observed[condseat.Finding] + for _, f := range found { + out.Findings = append(out.Findings, c.body(f)) + } + return out, nil + }, + }) + if err != nil { + return nil, err + } + c.w = w + // A writer that cannot start (the stream of its asks is not there yet) is said; its findings are then kept in the + // state alone and said by the next start (as the node-engine's is). + if err := w.Start(ctx); err != nil { + logf("the finding writer cannot start, its findings are kept in the state alone: %v", err) + } + return c, nil +} + +// body lays the checker's finding out as the seat's contract type: a finding of the module, about the module on its node. +func (c *checkerFindings) body(f views.Finding) condseat.Finding { + module, _ := c.w.ModuleNode() + return condseat.Finding{Kind: f.Kind, Scope: views.FindingScope, ID: keys.EscapeName(f.ID), At: f.At, Since: f.At, + Severity: condseat.FindingSeverityWarning, Source: f.Kind, Summary: f.Summary, Headline: f.Headline, Explanation: f.Explanation, + Subject: condseat.Subject{Scope: views.FindingScope, ID: module, Machine: c.node}} +} + +// Put writes one finding; its `since` stays where its key exists. +func (c *checkerFindings) Put(f views.Finding) error { + return c.w.Write(context.Background(), c.body(f)) +} + +// Delete clears one finding. +func (c *checkerFindings) Delete(f views.Finding) error { + return c.w.Clear(context.Background(), views.FindingScope, keys.EscapeName(f.ID), f.Kind, "the checker no longer observes it") +} + +var _ views.FindingWriter = (*checkerFindings)(nil) + +// readerFindings is the views reader's findings through the same writer, as kinds of the reader's own. +func (c *checkerFindings) readerFindings() views.FindingWriter { + return views.WriterFindings{Writer: c.w} +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index c82a599e..61cf7263 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -15,11 +15,10 @@ import ( "os" "os/signal" "strings" + "sync/atomic" "syscall" "time" - sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings" - "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" @@ -77,7 +76,6 @@ func durationEnv(getenv func(string) string, name string, def time.Duration) (ti // ModulesBucket, ManifestsBucket). var ( firstViewsBucket = check.CheckerModule + "_" + views.StatusState - findingsBucket = check.CheckerModule + "_findings" ) const ( @@ -163,8 +161,15 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro hub := process.NewEventHub(logger.Printf) reader := &views.ProcessReader{Module: check.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub} seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)} - findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf) - reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) } + // The finding writer is made once the checker is (its observe reads the checker's kept checks); until then a first view + // unanswered is said in the log alone. + var findings atomic.Pointer[checkerFindings] + reader.Findings = func() views.FindingWriter { + if f := findings.Load(); f != nil { + return f.readerFindings() + } + return nil + } // Each by the owner's read tool and views family as the owner's manifest names them. var followed error followOwner := func(owner, state, tool, stream string) *views.Picture { @@ -236,13 +241,19 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Owed: kvStore{buckets[check.CheckerOwedBucket]}, // A check that could not run waits for its part's running, and is the finding check-could-not-run after its bound. CouldNotRun: awaitedKV{couldNotRunKV}, - Findings: sdkfindings.Writer(ctx, findings.KV), Trunks: trunks, Bound: checkBound, Link: link, Logf: logger.Printf, Settle: settleWindow, } + cf, err := newCheckerFindings(ctx, cred, conn, js, caller.State("findings"), ch.RaisedCouldNotRun, logger.Printf) + if err != nil { + return fmt.Errorf("the checker's finding writer: %w", err) + } + defer cf.w.Close() + ch.Findings = cf + findings.Store(cf) kept := make(chan struct{}, 1) ch.CouldNotRunKept = func() { select { diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index f568baf3..73fb6820 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -44,7 +44,6 @@ replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 require ( - git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkermsg v0.0.0-00010101000000-000000000000 ) @@ -53,7 +52,7 @@ replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkerms replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk require ( - git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 // indirect diff --git a/modules/mesh-delivery-checker/internal/check/checker_test.go b/modules/mesh-delivery-checker/internal/check/checker_test.go index 68bb4c5d..51e8775f 100644 --- a/modules/mesh-delivery-checker/internal/check/checker_test.go +++ b/modules/mesh-delivery-checker/internal/check/checker_test.go @@ -301,10 +301,13 @@ func TestACheckThatCouldNotRunIsAskedAgainWhenItsPartRuns(t *testing.T) { // N6/ if due := r.ch.CouldNotRunDue(ctx, now.Add(CouldNotRunBound)); !due.IsZero() || found[key].Kind != CouldNotRunKind || !strings.Contains(found[key].Summary, "forge on g14") { t.Fatalf("at its bound: due %v, findings %+v, want %s naming the part", due, found, CouldNotRunKind) } + if observed, err := r.ch.RaisedCouldNotRun(ctx); err != nil || len(observed) != 1 || observed[0].Kind != CouldNotRunKind || observed[0].ID != key { + t.Fatalf("observed at its bound = %+v, %v, want its one finding, for the finding writer's statement", observed, err) + } r.ch.PartRunning(ctx, "g14", "forge", "src2:1") r.ch.Wait() - if len(found) != 0 { - t.Errorf("checked again, the finding stays: %v", found) + if observed, _ := r.ch.RaisedCouldNotRun(ctx); len(found) != 0 || len(observed) != 0 { + t.Errorf("checked again, the finding stays: %v, observed %v", found, observed) } }, "its heads replaced, or its pull request closed, the record and the finding go": func(t *testing.T) { diff --git a/modules/mesh-delivery-checker/internal/check/couldnotrun.go b/modules/mesh-delivery-checker/internal/check/couldnotrun.go index 003bc80e..643e06e7 100644 --- a/modules/mesh-delivery-checker/internal/check/couldnotrun.go +++ b/modules/mesh-delivery-checker/internal/check/couldnotrun.go @@ -233,6 +233,26 @@ func (ch *Checker) CouldNotRunDue(ctx context.Context, now time.Time) time.Time return next } +// RaisedCouldNotRun is every finding the checker observes now (design 47 G §384, §461): the check that could not run +// and whose finding is written. The finding writer states them at its start and when the keeper asks. +func (ch *Checker) RaisedCouldNotRun(ctx context.Context) ([]views.Finding, error) { + if ch.CouldNotRun == nil { + return nil, nil + } + keys, err := ch.CouldNotRun.Keys(ctx) + if err != nil { + return nil, fmt.Errorf("the checks that could not run cannot be listed: %w", err) + } + sort.Strings(keys) + var out []views.Finding + for _, key := range keys { + if r, _, ok := ch.couldNotRunOf(ctx, key); ok && r.Raised { + out = append(out, couldNotRunFinding(key, r, r.Since)) + } + } + return out, nil +} + // couldNotRunFinding is the finding of the check of key that could not run. func couldNotRunFinding(key string, r CouldNotRun, at time.Time) views.Finding { return views.Finding{Observer: CheckerModule, Kind: CouldNotRunKind, ID: key,