From 6301e49c7326ab9482a61d683ccbcfa9fd818dfe Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:07:11 +0100 Subject: [PATCH] Review round 2: a held module's object reads come from its running manifest; one with none keeps the list in force, said; indexer_judge asks the engine for the running build again (issue 774) --- .../internal/index/verdicts.go | 2 ++ .../internal/index/verdicts_test.go | 8 +++++ modules/mesh-grants/grants_test.go | 33 +++++++++++++++++++ modules/mesh-grants/internal/grants/inputs.go | 16 +++++++++ .../mesh-grants/internal/grants/records.go | 2 +- 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index 6b812027..6d79ec5a 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -383,6 +383,8 @@ func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn { out.Answer = "the indexer holds no manifest of " + module return out } + // The build this program runs is asked again, so a start pass that missed the engine's answer is recovered without a restart. + ix.learnOwnBuild(ctx) if _, err := ix.JudgeAll(ctx, nil); err != nil { out.Answer = "the manifests could not be judged: " + err.Error() return out diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 24dab79a..28acdca8 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -252,7 +252,15 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if rows, _ := ListVerdicts(keys, get, "", true); len(rows) <= len(keys)-3 || rows[0].Module > rows[len(rows)-1].Module { t.Fatalf("indexer_verdicts, all: %d rows of %d verdicts", len(rows), len(keys)) } + // indexer_judge asks the engine for the running build again: a start pass that missed it is recovered (the trunk's value of + // the indexer is the newer one, so the field waits; asked and answered with that same build it is refused). + ix.ownFP = "" + ix.OwnBuild = func(context.Context) (string, error) { return "src2:" + strings.Repeat("a", 64), nil } + if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "refused") || ix.ownFP == "" { + t.Fatalf("indexer_judge did not learn the running build: %+v", got) + } ix.ownFP = ownFP + ix.OwnBuild = nil if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") { t.Fatalf("indexer_judge: %+v", got) } diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index c00d43ac..17e8db89 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -680,6 +680,22 @@ func TestGrants(t *testing.T) { if slices.Contains(web.Publish, "$JS.API.DIRECT.GET.KV_mesh-knowledge_sections.>") { t.Error("the object bucket's read is granted as a state's") } + // ADR 0601 clause 3: the owner's trunk manifest now waits for a field; its running build's manifest still declares the + // bucket, so web keeps its object grant. + fp := "src2:" + strings.Repeat("cd", 32) + b.put(grants.ReadModules, "mesh-knowledge", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(`{"module":"mesh-knowledge","version":"2"}`)), Repository: "novox/x", Commit: "c", + Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}}) + b.put(grants.ReadHave, "a.mesh-knowledge", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp}) + heldKey, _ := delivery.BuilderBuildKey("mesh-knowledge", fp) + b.put(grants.ReadBuilds, heldKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(knowledge))}) + if heldWeb := (func() grants.User { + us := shadowOf(t, b) + return us[slices.IndexFunc(us, func(u grants.User) bool { return u.User == "a.web" })] + })(); !slices.Contains(heldWeb.Publish, read) { + t.Errorf("a reader of a held module's object store lost its grant: %v", heldWeb.Publish) + } + b.manifestOf("mesh-knowledge", knowledge) // accepted again for the rest of the row // ADR 0592 §6, §25: the bucket past 75 percent of its cap is said, in one warning, and cleared once below; the history // stream at 10 percent is not. var said []asks.Condition @@ -2758,6 +2774,11 @@ func TestGrants(t *testing.T) { } b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}}) + // mod-r runs a build whose manifest is the accepted one: the held module's seats and streams are composed from it. + runFP := "src2:" + strings.Repeat("cd", 32) + b.put(grants.ReadHave, "b.mod-r", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: runFP}) + runKey, _ := delivery.BuilderBuildKey("mod-r", runFP) + b.put(grants.ReadBuilds, runKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(ctl))}) p := partOn(b) p.Objects = &fakeObjects{} var said []asks.Condition @@ -2799,6 +2820,18 @@ func TestGrants(t *testing.T) { t.Errorf("%s: the kept user was not said: %+v", held.name, last) } } + // A held module that runs no build the part can read declares nothing to anyone: the list in force is kept, said. + delete(b.states[grants.ReadHave], "b.mod-r") + b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused, + Problems: []string{"it is held"}, Unknown: []string{"core-credentials"}}) + b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen9", "generation": 9, "fingerprint": "sha256:9"}) + wasList, _, _ := p.Sealed() + res := p.Compose() + nowList, _, _ := p.Sealed() + if nowList.Digest != wasList.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "running manifest of mod-r") { + t.Errorf("a held module with no running manifest did not keep the list in force: %+v", res.Sealed) + } // Accepted again: the users compose from the manifest and the condition clears. b.manifestOf("mod-r", ctl) if p.Compose(); said[len(said)-1].State != asks.StateCleared { diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 029a37e5..82b6a39c 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -445,6 +445,22 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")}) } in.Held = heldManifests(r, in, cache) + // A held module placed on a node whose running manifest is not found declares no seat, stream or object bucket to + // anyone: composing past it would drop the grants of those who use them, so the list in force is kept (ADR 0601 + // clause 3), and the gap says which module and why. + for _, g := range in.Refused { + module, ok := strings.CutPrefix(g.Input, "manifest of ") + if _, found := in.Held[module]; !ok || found { + continue + } + for _, assigned := range in.Assigned { + if slices.Contains(assigned, module) { + in.Gaps = append(in.Gaps, Gap{Input: "running manifest of " + module, Failed: true, + Why: "its manifest is held and no running build's manifest is found, so the seats and streams it declares cannot be composed: the list in force is kept"}) + break + } + } + } in.Hashes = readHashes(r, &in.Gaps) // The seat mesh-enrolment's keys are every token since the controller's end (issue 1126). in.Enrolling, _ = readEnrolling(r, &in.Gaps) diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 28014fbd..62d42424 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -81,7 +81,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key}) } d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes) - d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, in.Manifests, values) + d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, declared, values) return d } d := declare(m)