From d86baebe9ab396e03d7393cfddfe3d96a35ea03e Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:20:33 +0200 Subject: [PATCH] A plan settles an asked build from the build records (hq issue 214) A merge to the controller's own repository replaces the controller in its first tier; the build that produced the new one was recorded, the plan never heard it, and it waited for ever with every later plan behind it. The record is the fact: a build recorded after the ask is the tier's outcome, whoever was listening when it came. --- cmd/mesh-controller/release_plan.go | 56 ++++++++++++++++++++++++ cmd/mesh-controller/release_plan_test.go | 37 ++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index 438ea38..26a082c 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -399,6 +399,24 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan, } return true, nil } + // **Asked: settle from the build records first** (novox/hq 04-ISSUES/214). An outcome is taken + // in by whichever controller hears it, and a merge to the controller's own repository replaces + // the controller in its first tier: the build that produced the new one is recorded, and the + // plan never hears it. The record is the fact; a build recorded after the ask is that tier's + // outcome, whoever was listening. + recorded := map[string][]inventory.Build{} + for _, m := range tier { + if s := p.Modules[m]; s != nil && s.State == "asked" { + builds, err := inv.Builds(ctx, m, 5) + if err != nil { + return false, err + } + recorded[m] = builds + } + } + if settleFromRecords(p, tier, recorded) { + return true, nil + } // Asked: wait for every build. var latest time.Time for _, m := range tier { @@ -755,3 +773,41 @@ func splitList(s string) []string { } return out } + +// settleFromRecords marks every module of the tier still `asked` built — or failed — from a build +// recorded after it was asked, and says whether it changed anything (novox/hq 04-ISSUES/214). +// Newest first, as Builds answers: the first record after the ask is the outcome of that ask. +func settleFromRecords(p *inventory.Plan, tier []string, recorded map[string][]inventory.Build) bool { + changed := false + for _, m := range tier { + s := p.Modules[m] + if s == nil || s.State != "asked" || s.AskedAt == nil { + continue + } + var outcome *inventory.Build + for i := range recorded[m] { + b := recorded[m][i] + if b.At.Before(*s.AskedAt) { + break + } + outcome = &b + } + if outcome == nil { + continue + } + at := outcome.At + if outcome.Worked() { + s.State = "built" + s.BuiltAt = &at + s.Commit = outcome.Commit + } else { + s.State = "failed" + s.Why = outcome.Failed + p.State = inventory.PlanFailed + p.Note = fmt.Sprintf("%s failed to build in tier %d", m, p.Tier) + } + fmt.Printf("%s: %s settled from the build records as %s (%s)\n", p.ID, m, s.State, outcome.ID) + changed = true + } + return changed +} diff --git a/cmd/mesh-controller/release_plan_test.go b/cmd/mesh-controller/release_plan_test.go index c2eeb27..c4428ca 100644 --- a/cmd/mesh-controller/release_plan_test.go +++ b/cmd/mesh-controller/release_plan_test.go @@ -115,3 +115,40 @@ func TestACycleIsOneLastTierAndSaidSo(t *testing.T) { t.Fatalf("a cycle should be one tier of two, said: %v", tiers) } } + +// novox/hq 04-ISSUES/214: a plan whose build outcome was recorded while no controller followed it — +// the controller rebuilding itself — settles from the build records instead of waiting for ever. +func TestAPlanSettlesAnAskedBuildFromTheRecords(t *testing.T) { + asked := time.Date(2026, 10, 3, 19, 20, 0, 0, time.UTC) + p := inventory.Plan{ID: "plan-1", Tiers: [][]string{{"mesh-controller", "builder"}, {"route-proxy"}}, + Modules: map[string]*inventory.PlanModule{ + "mesh-controller": {State: "asked", AskedAt: &asked}, + "builder": {State: "asked", AskedAt: &asked}, + }} + records := map[string][]inventory.Build{ + // Newest first, as Builds answers: the build after the ask is the outcome. + "mesh-controller": { + {ID: "build-2", Commit: "2ebbb799", At: asked.Add(4 * time.Minute)}, + {ID: "build-1", Commit: "06ea2168", At: asked.Add(-10 * time.Minute)}, + }, + // Only a build from before the ask: not this ask's outcome. + "builder": {{ID: "build-0", Commit: "06ea2168", At: asked.Add(-time.Hour)}}, + } + if !settleFromRecords(&p, p.Tiers[0], records) { + t.Fatal("nothing settled, though the controller's build is recorded after the ask") + } + if s := p.Modules["mesh-controller"]; s.State != "built" || s.Commit != "2ebbb799" || s.BuiltAt == nil { + t.Errorf("the controller's ask is %+v, want built from 2ebbb799", s) + } + if s := p.Modules["builder"]; s.State != "asked" { + t.Errorf("an ask with no record after it was settled: %+v", s) + } + + // A failure recorded after the ask fails the plan, as hearing it would have. + q := inventory.Plan{ID: "plan-2", Tiers: [][]string{{"x"}}, + Modules: map[string]*inventory.PlanModule{"x": {State: "asked", AskedAt: &asked}}} + settleFromRecords(&q, q.Tiers[0], map[string][]inventory.Build{"x": {{ID: "b", Failed: "no", At: asked.Add(time.Minute)}}}) + if q.State != inventory.PlanFailed || q.Modules["x"].State != "failed" { + t.Errorf("a recorded failure did not fail the plan: %+v %+v", q, q.Modules["x"]) + } +}