From 672d1f4ca104b1185ff0babc999b4e1535b67014 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 5 Oct 2026 18:01:11 +0200 Subject: [PATCH] Leave an announced move to the plan rolling the module out (hq issue 249) The catalogue's upgrade announcement sent every machine one after another without waiting for any to apply, beside the plan that now sends one machine first. A module an open plan has not finished sending is the plan's to roll out. --- cmd/mesh-controller/rollout_first_test.go | 19 +++++++++++++++++++ cmd/mesh-controller/upgrades.go | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/cmd/mesh-controller/rollout_first_test.go b/cmd/mesh-controller/rollout_first_test.go index 15587bb..cb8ce79 100644 --- a/cmd/mesh-controller/rollout_first_test.go +++ b/cmd/mesh-controller/rollout_first_test.go @@ -82,3 +82,22 @@ func TestAPlanSendsOneMachineFirstAndTheRestAfterItsReport(t *testing.T) { t.Fatalf("a module on one machine waited for more: %+v", step) } } + +// An announced move of a module an open plan is still rolling out is left to the plan: sending it +// here as well put the bundle on every machine at once (novox/hq issue 249). +func TestAnAnnouncedMoveIsLeftToThePlanRollingItOut(t *testing.T) { + sent := time.Now() + plans := []inventory.Plan{ + {ID: "plan-done", State: inventory.PlanDone, Modules: map[string]*inventory.PlanModule{"agent": {}}}, + {ID: "plan-1", State: inventory.PlanRolling, Modules: map[string]*inventory.PlanModule{ + "agent": {State: "built", First: []string{"ace"}, FirstAt: &sent}, "gitea": {State: "built", SentAt: &sent}}}, + } + if got := rolledOutByAPlan(plans, "agent"); got != "plan-1" { + t.Fatalf("a module the plan is rolling out was not left to it: %q", got) + } + for _, m := range []string{"gitea", "keycloak"} { + if got := rolledOutByAPlan(plans, m); got != "" { + t.Errorf("%s, which no plan will send, was left to %s", m, got) + } + } +} diff --git a/cmd/mesh-controller/upgrades.go b/cmd/mesh-controller/upgrades.go index 50389b6..3c71189 100644 --- a/cmd/mesh-controller/upgrades.go +++ b/cmd/mesh-controller/upgrades.go @@ -56,6 +56,18 @@ func (f following) Upgraded(ctx context.Context, u link.Upgraded) error { return nil } + // **A plan that holds the module rolls it out, and this does not** (novox/hq issue 249, ADR + // 0218). A merge's plan builds the module and sends it one machine first, the rest once that one + // has applied it; this announcement arrives as the build registers, and sending here too — one + // machine after another without waiting for any to apply — put the new bundle on every machine in + // the same minute, whatever the plan was waiting for. A move no plan answers (a build asked by + // hand) is still this handler's. + if plans, err := inv.OpenPlans(ctx); err != nil { + return notNow(err) + } else if id := rolledOutByAPlan(plans, u.Module); id != "" { + fmt.Printf("%s moved to %s; %s rolls it out to %s\n", u.Module, shortCommit(u.Commit), id, readableList(on)) + return nil + } if decision.Together { fmt.Printf("%s moved to %s; sending %s together\n", u.Module, shortCommit(u.Commit), readableList(on)) @@ -77,6 +89,17 @@ func (f following) Upgraded(ctx context.Context, u link.Upgraded) error { return nil } +// rolledOutByAPlan is the open plan that will send a module's machines its new build — one holding +// the module that has not finished sending it — or empty when none will (novox/hq issue 249). +func rolledOutByAPlan(plans []inventory.Plan, module string) string { + for _, p := range plans { + if s, holds := p.Modules[module]; p.Open() && holds && (s == nil || s.SentAt == nil) { + return p.ID + } + } + return "" +} + // readableList names machines the way a sentence does, because this is read by a person deciding // whether an upgrade went where they expected. func readableList(names []string) string {