diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go index 7361c41b..6eb57494 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -27,9 +27,11 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState, counts func() str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) } return []stdio.Tool{ {Name: index.OldSectionsTool, - Description: "How many accepted modules use each old section of a manifest (state, logs, streams, objects), the foundation " + - "(nats, mesh-grants, the indexer) left out: the count the judge refuses a use of a section at 0 by, as modules move " + - "their bus state into `requires` by template. Read from the indexer's oldsections state, kept as the indexer judges " + + Description: "How many accepted modules use each old section of a manifest (state, logs, streams, objects), the nine parts of " + + "the foundation left out (ADR 0619): the count the judge refuses a use of a section at 0 by, as modules move " + + "their bus state into `requires` by template; and, under reads, the `reads` entries the accepted manifests still declare " + + "on the indexer's and the assigner's state, by owner and by entry, which reads 0 for each owner once every reader asks the " + + "owners' tools instead (ADR 0615 §1). Read from the indexer's oldsections state, kept as the indexer judges " + "the manifests; an empty answer means the indexer has not judged since it started.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, Run: func(map[string]any) (any, error) { @@ -37,7 +39,7 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState, counts func() if err != nil { return nil, err } - return c.Counts, nil + return map[string]any{"sections": c.Counts, "reads": c.Reads}, nil }}, {Name: index.TakeInTool, Description: "Take a registered module in now: the indexer reads its manifest from the repository and commit the controller's " + diff --git a/modules/mesh-delivery-indexer/internal/index/oldsections.go b/modules/mesh-delivery-indexer/internal/index/oldsections.go index 91d16b98..111f49f1 100644 --- a/modules/mesh-delivery-indexer/internal/index/oldsections.go +++ b/modules/mesh-delivery-indexer/internal/index/oldsections.go @@ -28,6 +28,9 @@ const ( type OldSectionsCount struct { V int `json:"v"` Counts map[string]int `json:"counts"` + // Reads are the `reads` entries left on the indexer's and the assigner's state, by owner and by entry, across the + // accepted manifests (judge.CountOwnerReads; ADR 0615 §1): 0 for each owner once every reader moved. + Reads map[string]int `json:"reads,omitempty"` } // DecodeOldSections reads the oldsections value. @@ -60,17 +63,17 @@ func (ix *Indexer) heldOldSections(ctx context.Context) judge.OldSectionCounts { } // keepOldSections writes the counts under OldSectionsKey when they differ from the ones held. -func (ix *Indexer) keepOldSections(ctx context.Context, counts judge.OldSectionCounts) error { +func (ix *Indexer) keepOldSections(ctx context.Context, counts judge.OldSectionCounts, reads map[string]int) error { if ix.OldSections == nil { return nil } return write(ctx, ix.OldSections, OldSectionsKey, func(value []byte, found bool) ([]byte, bool, error) { if found { - if cur, err := DecodeOldSections(value); err == nil && maps.Equal(cur.Counts, counts) { + if cur, err := DecodeOldSections(value); err == nil && maps.Equal(cur.Counts, counts) && maps.Equal(cur.Reads, reads) { return nil, false, nil } } - next, err := json.Marshal(OldSectionsCount{V: 1, Counts: counts}) + next, err := json.Marshal(OldSectionsCount{V: 1, Counts: counts, Reads: reads}) return next, false, err }) } diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index 1263a8e1..30454f1e 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -189,7 +189,7 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, over ma if over != nil { return out, nil } - if err := ix.keepOldSections(ctx, judge.CountOldSections(accepted)); err != nil { + if err := ix.keepOldSections(ctx, judge.CountOldSections(accepted), judge.CountOwnerReads(accepted)); err != nil { ix.logf("judge: the count of the old sections is not kept: %v", err) } return out, nil diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index b8ce4a49..ee2caab7 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -11,6 +11,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + + "mesh-delivery-indexer/judge" ) // The indexer judges every manifest it holds and writes each verdict only when it differs; a module's value carries a @@ -179,6 +181,29 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if c := counts(); c["state"] != 1 || c["logs"] != 0 || c["streams"] != 0 || c["objects"] != 0 { t.Fatalf("the old sections count %v, want state 1 and the rest 0", c) } + // DLV-J2, DLV-J3 (ADR 0615 §1): one reads entry on the assigner's state counts 1 for the assigner and 0 for the indexer. + hold("reader", `{"module":"reader","version":"1","state":["x"],"reads":["mesh-delivery-assigner.wants"]}`) + if _, err := ix.JudgeAll(ctx, nil); err != nil { + t.Fatal(err) + } + if value, _, _, _ := ix.OldSections.Get(ctx, OldSectionsKey); true { + c, _ := DecodeOldSections(value) + if c.Reads["mesh-delivery-assigner"] != 1 || c.Reads["mesh-delivery-assigner.wants"] != 1 || c.Reads["mesh-delivery-indexer"] != 0 { + t.Fatalf("the reads on the owners' state count %v", c.Reads) + } + } + // DLV-J1 (ADR 0619 §1, §3): a foundation part asking bus state in the new form is refused; the nine are the foundation. + cfg := hold("mesh-config", `{"module":"mesh-config","version":"1","requires":[{"name":"cache","storage":"key-value","template":"state"}]}`) + if _, err := ix.JudgeAll(ctx, map[string]bool{cfg: true}); err != nil { + t.Fatal(err) + } + if v := verdict(cfg); !v.Refused() || !strings.Contains(strings.Join(v.Problems, " "), "part of the foundation") { + t.Fatalf("mesh-config asking new-form bus state: %+v", v) + } + if !slices.Equal(judge.Foundation, []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools", + "mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault"}) { + t.Fatalf("the foundation is %v, want exactly the nine of ADR 0619", judge.Foundation) + } late := hold("late", `{"module":"late","version":"1","logs":[{"name":"l"}]}`) grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2","logs":[{"name":"l"},{"name":"m"}]}`) if _, err := ix.JudgeAll(ctx, map[string]bool{late: true, grants: true}); err != nil { diff --git a/modules/mesh-delivery-indexer/judge/oldsections.go b/modules/mesh-delivery-indexer/judge/oldsections.go index 47181176..b3aa5709 100644 --- a/modules/mesh-delivery-indexer/judge/oldsections.go +++ b/modules/mesh-delivery-indexer/judge/oldsections.go @@ -3,6 +3,7 @@ package judge import ( "fmt" "slices" + "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" ) @@ -15,8 +16,36 @@ import ( // OldSections are the sections of a manifest that keep bus state in the old forms. var OldSections = []string{"state", "logs", "streams", "objects"} -// Foundation are the modules that keep using the old sections for good. -var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer"} +// Foundation are the modules that keep using the old sections for good: exactly the nine parts the bus module's own +// composition, placement, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3): the bus module, the +// grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config and the vault. Their new +// state goes on their old sections too, so a foundation manifest asking bus state in the new form is refused. +var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools", + "mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault"} + +// ViewOwners are the two owners whose state every reader reads through their first views and read tools, never by a +// `reads` entry (novox/hq ADR 0615 §1): CountOwnerReads counts what is left of those entries. +var ViewOwners = []string{"mesh-delivery-indexer", "mesh-delivery-assigner"} + +// CountOwnerReads counts the `reads` entries on each view owner's state, by owner and by entry, across manifests: what +// the live row of the move reads 0 of (novox/hq ADR 0615 How it is checked). Every owner is in the answer, 0 where none. +func CountOwnerReads(manifests []catalogue.Manifest) map[string]int { + counts := map[string]int{} + for _, o := range ViewOwners { + counts[o] = 0 + } + for _, m := range manifests { + for _, r := range m.Reads { + for _, o := range ViewOwners { + if strings.HasPrefix(r, o+".") { + counts[o]++ + counts[r]++ + } + } + } + } + return counts +} // OldSectionCounts is, per old section, how many accepted modules outside the foundation use it. Nil is no count: the // judge then refuses no use of a section. @@ -54,7 +83,14 @@ func CountOldSections(manifests []catalogue.Manifest) OldSectionCounts { // OldSectionProblems are the old sections a manifest uses that no accepted module outside the foundation uses any more. func OldSectionProblems(m catalogue.Manifest, counts OldSectionCounts) []string { - if counts == nil || slices.Contains(Foundation, m.Module) { + if slices.Contains(Foundation, m.Module) { + if len(m.BusState) > 0 { + return []string{fmt.Sprintf("%s is a part of the foundation, which keeps its bus state in the old sections, new "+ + "entries included: it asks none in `requires` by template (novox/hq ADR 0619 §1)", m.Module)} + } + return nil + } + if counts == nil { return nil } var problems []string