From fe025a3b73aefa3842945a02bee79e681dc4a635 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 20:23:44 +0100 Subject: [PATCH] A check that could not run is asked again on the node-engine seat's running of its part (milestone 47/G W6.9b, issue 1373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checker consumes node-engine.running (kept last per subject) through its durable consumer and hands each running.. to PartRunning; a body naming no part asks nothing and is said. Naming the not-heard part by its module (the seat git's holder, a build seat's holder) waits on the catalog half's claims (W3.6): until then the part is unnamed and the first running asks the check again. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ Builds: design 47 G §253, §315 --- .../cmd/mesh-delivery-checker/couldnotrun.go | 21 +++++++++++++++ .../cmd/mesh-delivery-checker/forge_test.go | 26 +++++++++++++++++++ .../cmd/mesh-delivery-checker/main.go | 15 +++++++++++ .../mesh-delivery-checker/manifest_test.go | 2 +- modules/mesh-delivery-checker/module.json | 3 ++- 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/couldnotrun.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/couldnotrun.go index 4d3a0eff..e747f8dd 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/couldnotrun.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/couldnotrun.go @@ -2,8 +2,11 @@ package main import ( "context" + "encoding/json" "time" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "mesh-delivery-checker/internal/check" ) @@ -24,3 +27,21 @@ func waitCouldNotRun(ctx context.Context, ch *check.Checker, kept <-chan struct{ done() } } + +// partRunner is what a `running` is handed to: check.Checker's PartRunning. +type partRunner interface { + PartRunning(ctx context.Context, node, module, fingerprint string) +} + +// heardRunning takes one node-engine seat's `running..` in, as the checker's durable consumer of its stream +// hands it (module.json consumes node-engine.running): the part named runs at its fingerprint, so every check that could not +// run for want of it is asked again once (check.Checker.PartRunning). A body that names no node, module or fingerprint asks +// nothing, said in the log. +func heardRunning(ctx context.Context, ch partRunner, body []byte, logf func(string, ...any)) { + var r nodeengine.RunningEvent + if err := json.Unmarshal(body, &r); err != nil || r.Node == "" || r.Module == "" || r.Fingerprint == "" { + logf("a running of the node-engine seat that names no node, module or fingerprint asks no check again: %s (%v)", body, err) + return + } + ch.PartRunning(ctx, r.Node, r.Module, r.Fingerprint) +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go index c60da5c0..378b94c6 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go @@ -66,4 +66,30 @@ func TestAForgeReadThatReachesNobodyIsUnheard(t *testing.T) { // 1624 C7: "nats: if !errors.As(err, &u) { t.Errorf("err = %v, want it marked as reaching nobody", err) } + // Issue 1373, milestone 47/G W6.9: the check that could not run is asked again when the part runs, heard as the + // node-engine seat's running.. on the checker's consumer; a running that names no part asks nothing. + t.Run("asked again on running", func(t *testing.T) { + for _, c := range []struct { + body string + want []string + }{ + {`{"v":1,"node":"novox","module":"gitea","fingerprint":"src2:ab","manifest":"sha256:cd","at":"2026-11-09T20:00:00Z"}`, []string{"novox gitea src2:ab"}}, + {`{"v":1,"node":"novox","module":"gitea"}`, nil}, + {`not json`, nil}, + } { + var got []string + heardRunning(context.Background(), runnerFunc(func(_ context.Context, node, module, fp string) { + got = append(got, node+" "+module+" "+fp) + }), []byte(c.body), t.Logf) + if strings.Join(got, ",") != strings.Join(c.want, ",") { + t.Errorf("%s asked %v, want %v", c.body, got, c.want) + } + } + }) +} + +type runnerFunc func(ctx context.Context, node, module, fingerprint string) + +func (f runnerFunc) PartRunning(ctx context.Context, node, module, fingerprint string) { + f(ctx, node, module, fingerprint) } 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 c03b9705..c82a599e 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -23,6 +23,8 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "mesh-delivery-checker/internal/check" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" @@ -354,6 +356,19 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } defer cc.Stop() } + // The node-engine seat's running.., kept last per subject (design 47): a part a check could not hear runs, + // and the check is asked again (issue 1373). Its stream and the checker's durable consumer on it are made from the + // manifest's consumes; where they are not there yet the checker says so, and a check that could not run waits for its + // bound and is said as a finding. + if cons, err := cred.EventConsumer(ctx, js, nodeengine.StreamRunning); err != nil { + logger.Printf("the consumer %s_%s on %s does not exist yet, so no check that could not run is asked again until the checker starts again: %v", cred.Node, cred.Module, nodeengine.StreamRunning, err) + } else { + cc, err := process.HearEvents(cons, func(m jetstream.Msg) { heardRunning(ctx, ch, m.Data(), logger.Printf) }, logger.Printf) + if err != nil { + return err + } + defer cc.Stop() + } errs := make(chan error, 8) // The watchman's presence: a node coming back online asks again a check whose forge was not heard (UnheardWords), and // the assigner's own presence says whether a composition is being made (compositionWatch, ADR 0537 §4). A check is no diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index 67024589..bf7c60c6 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -141,7 +141,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 // The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts. if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", - "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions"}; !reflect.DeepEqual(sorted(m.Consumes), want) { + "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "node-engine.running"}; !reflect.DeepEqual(sorted(m.Consumes), want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" { diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index c3a9656f..f544c2f8 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -27,7 +27,8 @@ "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", - "mesh-conditions.findings-asked" + "mesh-conditions.findings-asked", + "node-engine.running" ], "streams": [ { "name": "checks", "events": ["preview.**", "owed.**", "try.**"] },