From 877b09e92b308d52b1cf6a6d80f8916f14cb1a6d Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:51:09 +0100 Subject: [PATCH] =?UTF-8?q?checker=5Fstatus=20names=20the=20builds=20a=20r?= =?UTF-8?q?unning=20check=20waits=20on=20(ADR=200603=20=C2=A710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worked out from the builder's builds state at each read, stored nowhere: of each running check, the candidate's builds that are neither done nor failed. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../cmd/mesh-delivery-checker-tools/main.go | 6 ++- .../internal/check/read.go | 49 +++++++++++++++++++ .../internal/check/read_test.go | 21 +++++++- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index 2159d0e5..371003d2 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -244,7 +244,9 @@ func Tools(r check.Reader) []stdio.Tool { "build seats' streams, whose backlog the delivery view shows (novox/hq ADR 0525). held_by_turn names the candidates whose check an approved " + "merge's turn holds off (novox/hq ADR 0548 §3), with the repository and the merge that waits; the gate's turns tool reads every turn and its " + "hand-now tool steers one. dead_letters says whether the checker hears the asks the build seats give up on (issue 734), and why not: " + - "deaf, a try or check whose ask is given up waits until its bound. Read-only.", + "deaf, a try or check whose ask is given up waits until its bound. builds_not_ended names, of each running check, the builder's builds of " + + "its candidate's changed modules that are neither done nor failed: the check waits on them before it composes, within its build-wait-bound " + + "(novox/hq ADR 0603 §10); a failed one fails the check: the builder's builder_build_again builds it again, then mesh-delivery.recheck asks the check again. Read-only.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, Run: func(map[string]any) (any, error) { return r.Status() }}, } @@ -283,7 +285,7 @@ func main() { StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))}, HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(), TurnState: read(stdio.State("mesh-delivery-gate.turns")), HearingState: read(stdio.State(buildrun.HearingState)), - AskState: read(stdio.State(check.AsksState))} + AskState: read(stdio.State(check.AsksState)), BuildState: read(stdio.State("mesh-delivery-builder.builds"))} r.Runs = runsOf{read(stdio.State("mesh-delivery-indexer.modules"))} // the node-engine at the commit the mesh runs, as the checker reads it tries := stdio.State(check.TriesState) source := stdio.State("mesh-delivery-gate.source") diff --git a/modules/mesh-delivery-checker/internal/check/read.go b/modules/mesh-delivery-checker/internal/check/read.go index 90e57f28..2086eced 100644 --- a/modules/mesh-delivery-checker/internal/check/read.go +++ b/modules/mesh-delivery-checker/internal/check/read.go @@ -3,6 +3,7 @@ package check import ( "encoding/json" "fmt" + "slices" "sort" "strings" "time" @@ -47,6 +48,9 @@ type Reader struct { Runs interface{ CommitOf(module string) string } // HearingState is the checker's state hearing: whether it hears DEAD_LETTERS (issue 734). HearingState StateRead + // BuildState is the builder's builds (mesh-delivery-builder.builds): the builds of a running check's candidate that have + // not ended, which the check waits on before it composes (ADR 0603 §10); without it none is named. + BuildState StateRead // AskState is the checker's state asks: the asks each running check put (AsksState), which checker_ask_again reads. AskState StateRead Now func() time.Time @@ -344,6 +348,10 @@ type Status struct { // Lost are the candidates whose verdict is running while no step of theirs can still run and their newest step ended, by // group, with that step: nothing will judge them (the lost check of 2026-10-28); checker_ask_again asks one again. Lost map[string]string `json:"lost"` + // BuildsNotEnded are, of each candidate whose check runs, the builder's builds of its changed modules that are neither + // done nor failed, by group: ` `, sorted (no key yet is `not begun`). A check waits on them before it + // composes (ADR 0603 §10), but for the modules its line names as composed without them; builder_build_again builds a failed one again. + BuildsNotEnded map[string]string `json:"builds_not_ended,omitempty"` // DeadLetters says whether the checker hears the asks the build seats give up on (issue 734): deaf, a try or check // whose ask is given up waits until its bound. DeadLetters DeadLettersHeard `json:"dead_letters"` @@ -500,6 +508,12 @@ func (r Reader) Status() (Status, error) { if why := read.lostWhy(c); why != "" { st.Lost[c.Group] = why } + if words := r.buildsNotEnded(c); words != "" { + if st.BuildsNotEnded == nil { + st.BuildsNotEnded = map[string]string{} + } + st.BuildsNotEnded[c.Group] = words + } case IsNodesFailure(v): st.Waiting[KindNodes] = append(st.Waiting[KindNodes], c.Group) case IsUntrunked(v): @@ -663,3 +677,38 @@ func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound } return "" } + +// buildsNotEnded are the builder's builds of the candidate's changed modules that are neither done nor failed, as +// Status.BuildsNotEnded says them; empty when none, or when the builder's builds are not readable here. +func (r Reader) buildsNotEnded(c delivery.Candidate) string { + if r.BuildState.Get == nil { + return "" + } + var out []string + for _, p := range c.Pulls { + for module, cm := range p.Fps { + if cm.FP2 == "" || cm.Manifest == "" { + continue + } + key, err := delivery.BuilderBuildKey(module, cm.FP2) + if err != nil { + continue + } + raw, err := r.BuildState.Get(key) + if err != nil { + continue + } + state := "not begun" + if len(raw) > 0 { + b, err := delivery.DecodeBuilderBuild(raw) + if err != nil || b.State == delivery.BuildDone || b.State == delivery.BuildFailed { + continue + } + state = b.State + } + out = append(out, module+" "+state) + } + } + sort.Strings(out) + return strings.Join(slices.Compact(out), ", ") +} diff --git a/modules/mesh-delivery-checker/internal/check/read_test.go b/modules/mesh-delivery-checker/internal/check/read_test.go index 9a866222..033ba635 100644 --- a/modules/mesh-delivery-checker/internal/check/read_test.go +++ b/modules/mesh-delivery-checker/internal/check/read_test.go @@ -147,10 +147,24 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { passed, _ := delivery.EncodeStep(delivery.Step{ID: "abc2", Tool: "build_go_test", Node: "ace", State: delivery.StepPassed}) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) ckey, _ := delivery.CandidatesKey("feat/x") + // ADR 0603 §10: feat/y's check runs and waits on the builds of its changed modules: web building, mail not begun; api + // is done and named nowhere. + fp, man := "src2:"+strings.Repeat("f", 64), strings.Repeat("e", 64) + py := pullOf(repoA, 8, head2) + py.Modules, py.Fps = []string{"api", "mail", "web"}, map[string]delivery.CandidateModule{"web": {FP2: fp, Manifest: man}, "mail": {FP2: fp, Manifest: man}, "api": {FP2: fp, Manifest: man}} + candY := candidateOf(t, "feat/y", py) + ykey, _ := delivery.CandidatesKey("feat/y") + runningV, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/y", Heads: "0123456789ab", State: delivery.VerdictRunning}) + builds := fakeState{} + for module, state := range map[string]string{"web": delivery.BuildBuilding, "api": delivery.BuildDone} { + k, _ := delivery.BuilderBuildKey(module, fp) + builds[k], _ = json.Marshal(delivery.BuilderBuild{V: 1, State: state}) + } rd := Reader{ StepStates: map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()}, - CandidateState: fakeState{ckey: cand}.read(), - VerdictState: fakeState{}.read(), + CandidateState: fakeState{ckey: cand, ykey: candY}.read(), + VerdictState: fakeState{keyOf(t, "feat/y", candY): runningV}.read(), + BuildState: builds.read(), HaveState: fakeState{"shanks.mesh-build-go": haveOf(t, delivery.PhaseRunning)}.read(), HearingState: fakeState{"dead-letters": []byte(`{"heard":false,"streams":["SEAT_NODE_BUILD_GO"],` + `"why":"DEAD_LETTERS is not heard, so an ask a build seat gives up on is waited for until its bound: SEAT_NODE_BUILD_GO: permissions violation",` + @@ -167,6 +181,9 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { if len(st.Unjudged) != 1 || st.Unjudged[0] != "feat/x" { t.Errorf("unjudged = %v", st.Unjudged) } + if got := st.BuildsNotEnded; len(got) != 1 || got["feat/y"] != "mail not begun, web building" { + t.Errorf("builds not ended = %v, want feat/y's mail not begun and web building", got) + } // Issue 734, review round 1: a checker that does not hear DEAD_LETTERS says so in its status, with why. if st.DeadLetters.Heard || !strings.Contains(st.DeadLetters.Why, "permissions violation") { t.Errorf("dead letters = %+v, want not heard, saying why", st.DeadLetters)