diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 746dc902..834e8c39 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -324,7 +324,8 @@ func main() { PackagesState: read(stdio.State(delivery.IndexerModule + "." + delivery.PackagesState)), GraphState: read(stdio.State(graph.Read)), SourceState: read(stdio.State(delivery.GateModule + ".source")), ProvisionsState: read(stdio.State(provisions.ProvisionsRead)), - BuildLog: stdio.Log(delivery.BuildLogRead).Since} + BuildLog: stdio.Log(delivery.BuildLogRead).Since, + Pending: pendingOf} u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")), Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())} for _, lang := range delivery.AskLanguages { @@ -337,3 +338,12 @@ func main() { os.Exit(1) } } + +// pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5). +func pendingOf() (delivery.PendingAnswer, error) { + raw, err := stdio.Ask(delivery.IndexerModule+".indexer_pending", map[string]any{}) + if err != nil { + return delivery.PendingAnswer{}, err + } + return delivery.DecodePendingAnswer(raw) +} diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go index bf2c81dd..c9f8bba5 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go @@ -102,6 +102,36 @@ func tiersToolSaysEachModulesWait(t *testing.T) { web.Waits[1].Edge != "provision database" || !strings.Contains(web.Waits[1].Words, "asked: the build db runs does not offer it") { t.Errorf("web = %+v, want after db, held on ace by the provision rule and its provision asked (g14 is away)", web) } + // ADR 0633 §5 (1585/T24): a module the indexer holds back, read through indexer_pending, waits on the edge take-in from the + // wait's since; a held module the revision does not move is named in the first tier with that wait alone, a group's wait + // says its group, and a tool that cannot be asked is said and names no module. + heldSince := time.Date(2026, 11, 4, 22, 0, 0, 0, time.UTC) + r.Pending = func() (delivery.PendingAnswer, error) { + return delivery.PendingAnswer{Waits: []delivery.PendingWait{ + {Key: "m.00000000000000000042", What: delivery.TakeInRevision, WaitsFor: "src2:abc", Since: heldSince, Holds: []string{"web", "newmod"}}, + {Key: "g.feat", What: delivery.TakeInGroup, Group: "feat", Since: heldSince.Add(time.Hour), Holds: []string{"lib"}}}}, nil + } + out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}) + held, _ := out.(view.Tiers) + if err != nil || len(held.Tiers) != 2 || held.Pending != "" { + t.Fatalf("view_tiers with waits = %+v, %v", out, err) + } + web = held.Tiers[1][0] + if last := web.Waits[len(web.Waits)-1]; last.Edge != view.TakeInEdge || !last.Since.Equal(heldSince) || !strings.Contains(last.Words, "src2:abc") { + t.Errorf("web = %+v, want its last wait on the edge take-in since %s", web, heldSince) + } + if lib := held.Tiers[0][1]; lib.Module != "lib" || lib.Waits[len(lib.Waits)-1].Edge != view.TakeInEdge || !strings.Contains(lib.Waits[len(lib.Waits)-1].Words, "group feat") { + t.Errorf("lib = %+v, want its last wait the group's", lib) + } + if nm := held.Tiers[0][len(held.Tiers[0])-1]; nm.Module != "newmod" || len(nm.Waits) != 1 || nm.Waits[0].Edge != view.TakeInEdge { + t.Errorf("the first tier = %+v, want newmod, which the revision does not move, named with its take-in wait alone", held.Tiers[0]) + } + r.Pending = func() (delivery.PendingAnswer, error) { return delivery.PendingAnswer{}, errors.New("no answer") } + if out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || !strings.Contains(out.(view.Tiers).Pending, "no answer") || + strings.Contains(fmt.Sprint(out.(view.Tiers).Tiers), view.TakeInEdge) { + t.Errorf("view_tiers with no answer of indexer_pending = %+v, %v", out, err) + } + r.Pending = nil // No revision named: the gate's newest, its source key's revision. r.SourceState = view.StateRead{GetEntry: func(string) (json.RawMessage, uint64, error) { return json.RawMessage(`{}`), 9, nil }} if out, err := run(t, r, view.TiersTool, map[string]any{}); err != nil || out.(view.Tiers).Revision != 9 || out.(view.Tiers).Modules != 3 { diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index 5da2b0bb..7e7afa6a 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -58,6 +58,8 @@ type Reader struct { // GraphState is the indexer's module graph (graph.Bucket, ADR 0603 §2), SourceState the gate's source and // ProvisionsState the provisioner's keys: what view_tiers reads (tiers.go). Unset reads none, said in the answer. GraphState, SourceState, ProvisionsState StateRead + // Pending reads the indexer's waits through its tool indexer_pending (ADR 0633 §5); unset names no module held back. + Pending func() (delivery.PendingAnswer, error) // BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none. BuildLog LogSince } diff --git a/modules/mesh-delivery-view/internal/view/tiers.go b/modules/mesh-delivery-view/internal/view/tiers.go index 35a1e0a7..59d05bf1 100644 --- a/modules/mesh-delivery-view/internal/view/tiers.go +++ b/modules/mesh-delivery-view/internal/view/tiers.go @@ -43,6 +43,8 @@ type Tiers struct { Graph string `json:"graph,omitempty"` Cycle []string `json:"cycle,omitempty"` Away []string `json:"away,omitempty"` + // Pending says why the indexer's waits could not be read, so no module is named held by its revision's indexer (ADR 0633 §5). + Pending string `json:"pending,omitempty"` } // TierModule is one module of a tier: its fingerprint, the modules of the same revision it waits for (After), each @@ -115,6 +117,7 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) { out.Away = slices.Sorted(maps.Keys(away)) wants, _ := values(r.WantsState) provided, perr := values(r.ProvisionsState) + held, heldAll := r.heldBack(&out) // The ordering among the revision's modules: each one's tier is one past the highest of the modules it waits for. in := map[string]bool{} @@ -177,8 +180,26 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) { m := moved[n] tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n], LeftOut: gr[n].LeftOut} tm.Waits = r.waitsOf(n, m, gr[n], wants, provided, perr, away) + if w, ok := held[n]; ok { + tm.Waits = append(tm.Waits, w) + } out.Tiers[t] = append(out.Tiers[t], tm) } + // A module the indexer holds back is not at the revision yet, so no tier of it names it: it is named in the first tier with + // its wait alone (ADR 0633 §5), with the fingerprint the indexer last took in. + for _, n := range heldAll { + if in[n] { + continue + } + tm := TierModule{Module: n, Waits: []TierWait{held[n]}} + if m, err := delivery.DecodeModule(modules[n]); err == nil { + tm.FP, tm.None = m.FP2, m.None2 + } + if len(out.Tiers) == 0 { + out.Tiers = append(out.Tiers, nil) + } + out.Tiers[0] = append(out.Tiers[0], tm) + } compact := out.Tiers[:0] for _, t := range out.Tiers { if len(t) > 0 { @@ -189,6 +210,37 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) { return out, nil } +// TakeInEdge is the edge of a wait on the indexer a source revision or a group is held for (ADR 0633 §5). +const TakeInEdge = "take-in" + +// heldBack are the modules the indexer holds back, by module, each with its wait, read through the indexer's tool +// indexer_pending (ADR 0633 §5), and the same modules sorted. A tool that cannot be asked says so in out.Pending. +func (r Reader) heldBack(out *Tiers) (map[string]TierWait, []string) { + held := map[string]TierWait{} + if r.Pending == nil { + return held, nil + } + a, err := r.Pending() + if err != nil { + out.Pending = "the indexer's pending revisions cannot be read, so no module is named waiting for its revision's indexer: " + err.Error() + return held, nil + } + for _, w := range a.Waits { + words := "the indexer holds back " + w.Key + ", which waits for the indexer build " + w.WaitsFor + if w.What == delivery.TakeInGroup { + words = "the indexer gathers the group " + w.Group + " and its end has not come" + } else if w.Behind != "" { + words = "the indexer holds back " + w.Key + " behind " + w.Behind + } + for _, module := range w.Holds { + if _, ok := held[module]; !ok { // the oldest wait names it + held[module] = TierWait{Edge: TakeInEdge, Words: words, Since: w.Since} + } + } + } + return held, slices.Sorted(maps.Keys(held)) +} + // away are the nodes the watchman's presence says are offline. func (r Reader) away() map[string]bool { out := map[string]bool{} diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 4e65b88f..b593a849 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -51,6 +51,7 @@ type manifest struct { Version string `json:"version"` Slug string `json:"slug"` Reads []string `json:"reads"` + Invokes []string `json:"invokes"` Uses []string `json:"uses"` Tools []string `json:"tools"` Emits []string `json:"emits"` @@ -122,6 +123,10 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } + // 1585/T24: view_tiers names a module waiting for its revision's indexer through the indexer's tool, a verb edge (ADR 0614 §2). + if !reflect.DeepEqual(m.Invokes, []string{"mesh-delivery-indexer.indexer_pending"}) { + t.Errorf("invokes = %v", m.Invokes) + } for _, a := range m.Build.Artifacts { // A bundle compiled to a binary is one executable: what the runtime loads is that binary's name, or the controller's // module check refuses the manifest (mesh-sdk catalogue build.go, ADR 0193). diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index 55bbbfb6..74236726 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -26,6 +26,9 @@ "indexer_heard", "view_tiers" ], + "invokes": [ + "mesh-delivery-indexer.indexer_pending" + ], "emits": [ "delivered", "moving",