From 7855ea126cfceff94a2b16cf5137c241262787d5 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 09:14:25 +0000 Subject: [PATCH] Ask over mesh-sdk's composition: a question makes no pair credential and reads the mesh once (issue 1387) The callers that only ask (status, the doctor's probes, facts, bindings, data, acts, nodes show, images, release, recorded push, consumers) resolve through resolveNode and compose through composeQuestion, over one Input read per operation (withInputCache). A question composes with Reading or Foreseeing and never calls PairCredential's maker: the credential a send would make is named, and perform makes it on a send. --- cmd/mesh-controller/accept_outside.go | 4 +++- cmd/mesh-controller/acts.go | 6 ++++-- cmd/mesh-controller/bindings.go | 4 +++- cmd/mesh-controller/data.go | 6 ++++-- cmd/mesh-controller/facts.go | 13 ++++++++----- cmd/mesh-controller/nodes.go | 2 +- cmd/mesh-controller/probes.go | 9 ++++++--- cmd/mesh-controller/recorded_push.go | 2 +- cmd/mesh-controller/registry_verbs.go | 2 +- cmd/mesh-controller/release.go | 2 +- cmd/mesh-controller/status.go | 6 ++++-- 11 files changed, 36 insertions(+), 20 deletions(-) diff --git a/cmd/mesh-controller/accept_outside.go b/cmd/mesh-controller/accept_outside.go index 3a389f74..2489f542 100644 --- a/cmd/mesh-controller/accept_outside.go +++ b/cmd/mesh-controller/accept_outside.go @@ -55,6 +55,8 @@ func offeredOutsideBy(ctx context.Context, provision string) (string, error) { // its next send would, and the machines that could not be resolved, with why. func consumersFrom(ctx context.Context, open *stores, provider, providerModule, provision string) ( []inventory.PairEnd, []string, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) nodes, err := open.inventory.Nodes(ctx) if err != nil { return nil, nil, err @@ -62,7 +64,7 @@ func consumersFrom(ctx context.Context, open *stores, provider, providerModule, var out []inventory.PairEnd var unresolved []string for _, n := range nodes { - plan, _, err := planFor(ctx, open, n.Name) + plan, _, err := resolveNode(ctx, open, n.Name) if err != nil { unresolved = append(unresolved, fmt.Sprintf("%s: %s", n.Name, oneLine(err.Error()))) continue diff --git a/cmd/mesh-controller/acts.go b/cmd/mesh-controller/acts.go index 3b0d11a0..3989f0f8 100644 --- a/cmd/mesh-controller/acts.go +++ b/cmd/mesh-controller/acts.go @@ -314,7 +314,7 @@ type afterAct struct { // after the write, which can only find more unmet than the closure would: a seat said held there is // held in the closure too. func resolvedAfter(ctx context.Context, open *stores, shelf map[string]catalogue.Manifest, node string) afterAct { - plan, _, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err == nil { out := afterAct{plan: plan, unheld: plan.Unheld} for _, m := range plan.Modules { @@ -355,6 +355,8 @@ func splitModules(field string) []string { // Nothing here can fail the act it reports on. A mesh that cannot be read is worth saying and is // not a reason to claim the assignment did not happen — it did. func blockedElsewhere(ctx context.Context, open *stores, except string) string { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) nodes, err := open.inventory.Nodes(ctx) if err != nil { return "\n\nThe rest of the mesh could not be checked: " + err.Error() @@ -364,7 +366,7 @@ func blockedElsewhere(ctx context.Context, open *stores, except string) string { if n.Name == except { continue } - if _, _, err := planFor(ctx, open, n.Name); err != nil { + if _, _, err := resolveNode(ctx, open, n.Name); err != nil { blocked[n.Name] = err.Error() } } diff --git a/cmd/mesh-controller/bindings.go b/cmd/mesh-controller/bindings.go index ee0370ad..aab111f1 100644 --- a/cmd/mesh-controller/bindings.go +++ b/cmd/mesh-controller/bindings.go @@ -73,6 +73,8 @@ func reportKept(ctx context.Context, plan catalogue.Resolution) { // sent, on every machine — the resolver kept it there (said, urgent, until a person pins), or a pin // moves it (said, so the data goes first), and never anything else. func probeBindings(ctx context.Context, d *doctor) ([]conditions.Observation, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) inv := d.open.inventory nodes, err := inv.Nodes(ctx) if err != nil { @@ -80,7 +82,7 @@ func probeBindings(ctx context.Context, d *doctor) ([]conditions.Observation, er } var out []conditions.Observation for _, n := range nodes { - plan, _, err := planFor(ctx, d.open, n.Name) + plan, _, err := resolveNode(ctx, d.open, n.Name) if err != nil { if unresolvable(err) { // D1 says it, with the binding that refused it when that is why. diff --git a/cmd/mesh-controller/data.go b/cmd/mesh-controller/data.go index 6c41e640..15a808c9 100644 --- a/cmd/mesh-controller/data.go +++ b/cmd/mesh-controller/data.go @@ -152,6 +152,8 @@ type consumerCopy struct { // probeData is D13. func probeData(ctx context.Context, d *doctor) ([]conditions.Observation, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) if d.js == nil { return nil, errors.New("no bus to ask the machines over") } @@ -180,7 +182,7 @@ func probeData(ctx context.Context, d *doctor) ([]conditions.Observation, error) } var machines []*machine for _, n := range nodes { - plan, _, err := planFor(ctx, open, n.Name) + plan, _, err := resolveNode(ctx, open, n.Name) if err != nil { if ctx.Err() != nil { return nil, ctx.Err() @@ -849,7 +851,7 @@ func deleteRetiredData(ctx context.Context, conn *nats.Conn, open *stores, r inv return fmt.Errorf("%s of %s on %s was never measured, so where it is was never said; nothing was deleted", r.Item, r.Module, r.Machine) } - if plan, _, err := planFor(ctx, open, r.Machine); err == nil { + if plan, _, err := resolveNode(ctx, open, r.Machine); err == nil { for _, m := range plan.Modules { if _, still := m.DataItem(r.Item); still && m.Module == r.Module { return fmt.Errorf("%s runs on %s again and declares %s: it is not retired any more. Nothing was done", diff --git a/cmd/mesh-controller/facts.go b/cmd/mesh-controller/facts.go index 83cbb7a7..5d9e9781 100644 --- a/cmd/mesh-controller/facts.go +++ b/cmd/mesh-controller/facts.go @@ -17,6 +17,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/validate" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" "github.com/novox/mesh-controller/internal/artifacts" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/builder" @@ -263,7 +264,9 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot f.Controller.Commit = c.Commit } - gens, gensErr := generators(ctx, open) + // The Input every machine's declaration below is composed over is read once for the whole snapshot. + ctx = withInputCache(ctx) + _, gensErr := generatorsOf(ctx, open) hostShelf := shelf[hostModule] meshWide := map[string]bool{} engines := map[string]bool{} @@ -361,7 +364,7 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot m.Settings = append(m.Settings, snapshot.Settings{Module: module, Values: scrub.Values(layer.Whole())}) } } - m.Declaration = declarationFacts(ctx, open, n.Name, gens, gensErr, scrub) + m.Declaration = declarationFacts(ctx, open, n.Name, gensErr, scrub) if ctx.Err() != nil { return snapshot.Facts{}, ctx.Err() } @@ -456,14 +459,14 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot // declarationFacts is how one machine's declaration composes now, as the next push would compose it and // without making anything (D1's composition), and whether the node-engine's validator takes it. -func declarationFacts(ctx context.Context, open *stores, node string, gens map[string]catalogue.Generator, - gensErr error, scrub *snapshot.Scrubber) snapshot.Declaration { +func declarationFacts(ctx context.Context, open *stores, node string, gensErr error, + scrub *snapshot.Scrubber) snapshot.Declaration { var d snapshot.Declaration if gensErr != nil { d.Problems = []string{scrub.Text("the private network cannot be computed: " + oneLine(gensErr.Error()))} return d } - declared, problems, err := composedAndValidated(ctx, open, node, gens, Foreseeing) + declared, problems, err := composeQuestion(ctx, open, node, compose.Foreseeing, true) if err != nil { d.Problems = []string{scrub.Text(oneLine(err.Error()))} return d diff --git a/cmd/mesh-controller/nodes.go b/cmd/mesh-controller/nodes.go index 6a30aba3..232bd89e 100644 --- a/cmd/mesh-controller/nodes.go +++ b/cmd/mesh-controller/nodes.go @@ -924,7 +924,7 @@ func showNode(ctx context.Context, stored *stores, name string) error { } // And which of them a push leaves out, and why (novox/hq issue 380): judged as the push judges it, the same // reason its condition says. Not computable is said, never read as "all applied". - plan, settings, err := planFor(ctx, stored, name) + plan, settings, err := resolveNode(ctx, stored, name) switch { case err != nil: fmt.Printf("\n whether a send leaves any assigned module out is NOT known: %s\n", oneLine(err.Error())) diff --git a/cmd/mesh-controller/probes.go b/cmd/mesh-controller/probes.go index c92e3659..b082fbb5 100644 --- a/cmd/mesh-controller/probes.go +++ b/cmd/mesh-controller/probes.go @@ -20,6 +20,7 @@ import ( "golang.org/x/net/dns/dnsmessage" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/artifacts" "github.com/novox/mesh-controller/internal/broker" @@ -62,7 +63,9 @@ func probeDeclarations(ctx context.Context, d *doctor) ([]conditions.Observation // The private network every declaration is composed with. Not computable is not "could not // look": it is the finding — no machine's declaration composes without it — and the machines that // do not resolve, which are usually why, are named below. - gens, gensErr := generators(ctx, open) + // Every machine below is composed over one reading of the mesh. + ctx = withInputCache(ctx) + _, gensErr := generatorsOf(ctx, open) if gensErr != nil { if ctx.Err() != nil { return nil, ctx.Err() @@ -73,7 +76,7 @@ func probeDeclarations(ctx context.Context, d *doctor) ([]conditions.Observation Said: firstLine(gensErr.Error())}) } for _, n := range nodes { - plan, settings, err := planFor(ctx, open, n.Name) + plan, settings, err := resolveNode(ctx, open, n.Name) if err != nil && !unresolvable(err) { return nil, fmt.Errorf("%s cannot be worked out: %w", n.Name, err) } @@ -85,7 +88,7 @@ func probeDeclarations(ctx context.Context, d *doctor) ([]conditions.Observation } if err == nil && gensErr == nil { var declared sendable - if declared, err = declarationWith(ctx, open, n.Name, plan, settings, gens, Foreseeing); err == nil { + if declared, _, err = composeQuestion(ctx, open, n.Name, compose.Foreseeing, false); err == nil { foreseen = declared.foreseen // With the order it was last sent, so the validator reads the envelope a machine is sent // — its epoch included (novox/hq to-be 45 §6). diff --git a/cmd/mesh-controller/recorded_push.go b/cmd/mesh-controller/recorded_push.go index 676dbd19..c6ca5de3 100644 --- a/cmd/mesh-controller/recorded_push.go +++ b/cmd/mesh-controller/recorded_push.go @@ -93,7 +93,7 @@ func recordedPushOf(ctx context.Context, node string) ([]string, string, error) } defer open.Close() inv := open.inventory - plan, _, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err != nil { return nil, "", fmt.Errorf("its set cannot be worked out: %w", err) } diff --git a/cmd/mesh-controller/registry_verbs.go b/cmd/mesh-controller/registry_verbs.go index 286cb7b0..c89f695d 100644 --- a/cmd/mesh-controller/registry_verbs.go +++ b/cmd/mesh-controller/registry_verbs.go @@ -400,7 +400,7 @@ func imagesCommand(ctx context.Context, args []string) error { } // What the mesh would send now, composed as `plan` composes it. var body []byte - plan, settings, err := planFor(ctx, open, node) + plan, settings, err := resolveNode(ctx, open, node) if err != nil { return err } diff --git a/cmd/mesh-controller/release.go b/cmd/mesh-controller/release.go index 5df0785a..ff063b71 100644 --- a/cmd/mesh-controller/release.go +++ b/cmd/mesh-controller/release.go @@ -247,7 +247,7 @@ func (f moveFacts) walkedBy(module, node, to string) string { // machineMoves is the moves no gate has seen on one machine, read from what it would be sent now. var machineMoves = func(ctx context.Context, open *stores, f moveFacts, node string, all bool) ([]inventory.CarriedMove, error) { - plan, _, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err != nil { return nil, nil // it cannot be worked out: the send says why } diff --git a/cmd/mesh-controller/status.go b/cmd/mesh-controller/status.go index 12f0c92c..5d262e36 100644 --- a/cmd/mesh-controller/status.go +++ b/cmd/mesh-controller/status.go @@ -406,6 +406,8 @@ type builds struct { // anything out of date. The first has consequences now, the second may, the third is a plan for // later — and anything that led with the third would bury the first. func theThreeQuestions(ctx context.Context, open *stores) (answers, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) inv := open.inventory var out answers var err error @@ -429,7 +431,7 @@ func theThreeQuestions(ctx context.Context, open *stores) (answers, error) { // // Read through whoResolves, which is what the private network is built from, so this and the // network agree about who could not be resolved rather than deciding it twice. - _, out.refused, err = whoResolves(ctx, open, overlay.Addressing) + _, out.refused, err = whoResolvesOn(ctx, open, overlay.Addressing) if err != nil { return answers{}, err } @@ -453,7 +455,7 @@ func theThreeQuestions(ctx context.Context, open *stores) (answers, error) { // resolve is already in refused, and is passed over here. plans := map[string]planned{} for _, n := range out.nodes { - plan, settings, err := planFor(ctx, open, n.Name) + plan, settings, err := resolveNode(ctx, open, n.Name) if err != nil { if unresolvable(err) { continue