View: view_tiers names a module the indexer holds back, read through indexer_pending (issue 1585, ADR 0633 §5)

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-04 23:09:20 +01:00
parent 72633722c5
commit d11a5af113
6 changed files with 103 additions and 1 deletions
@@ -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)
}
@@ -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 {
@@ -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
}
@@ -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{}
@@ -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).
+3
View File
@@ -26,6 +26,9 @@
"indexer_heard",
"view_tiers"
],
"invokes": [
"mesh-delivery-indexer.indexer_pending"
],
"emits": [
"delivered",
"moving",