diff --git a/cmd/mesh-controller/plan_retry.go b/cmd/mesh-controller/plan_retry.go index 8862da8d..7e846f77 100644 --- a/cmd/mesh-controller/plan_retry.go +++ b/cmd/mesh-controller/plan_retry.go @@ -218,7 +218,7 @@ func retryRefusal(p inventory.Plan, plans []inventory.Plan, onPath map[string]st } return oneWalkAtATime(p, plans, onPath) } - // **A walk lost to silence is resumed** (issue 1886): its tier was sent whole and only the gate after it failed it, every + // **A walk lost to silence is resumed** (novox/hq ADR 0531, issue 1886): its tier was sent whole and only the gate after it failed it, every // machine it waited on silent; once they reported, it goes on from that gate. if len(stopped) == 0 && lostToSilence(p) { if q, found := newerOpenPlan(p, plans); found { @@ -718,11 +718,11 @@ func resumeStopped(p *inventory.Plan) string { } // silentGateWords say, in a plan's note, that the gate between two tiers found every machine it waits on silent past -// ApplySilentAfter, and failed the plan as lost (issue 1160): one text for the writer and the reader (lostToSilence). +// ApplySilentAfter, and failed the plan as lost (issue 1160, ADR 0531): one text for the writer and the reader (lostToSilence). const silentGateWords = "every machine it waits on said nothing on its send within" -// lostToSilence is whether a plan failed only at the gate after its tier, every machine it waited on silent (issue -// 1886): nothing in its tier failed to build, stopped rolling out or is still to ask — the look had sent the whole tier. +// lostToSilence is whether a plan failed only at the gate after its tier, every machine it waited on silent (ADR 0531, +// issue 1886): nothing in its tier failed to build, stopped rolling out or is still to ask — the look had sent the whole tier. func lostToSilence(p inventory.Plan) bool { if p.State != inventory.PlanFailed || p.Tier >= len(p.Tiers) || !strings.Contains(p.Note, silentGateWords) || len(failedIn(p)) > 0 || len(stoppedRollouts(p)) > 0 { @@ -732,7 +732,7 @@ func lostToSilence(p inventory.Plan) bool { return asked == 0 && unasked == 0 } -// resumeLost sets a walk lost to silence building again at the tier it failed after (issue 1886), for the next look to +// resumeLost sets a walk lost to silence building again at the tier it failed after (ADR 0531, issue 1886), for the next look to // judge its gate again on the machines' reports, unless every machine its tier was sent to is still silent: then // nothing it needs is delivered anywhere yet, and the retry is refused as the gate would fail it again. func resumeLost(p *inventory.Plan, silent map[string]bool) (string, error) { diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index a1d49edf..54336966 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -260,7 +260,7 @@ type gateNeed struct { } // silentNodes is the machines a walk sent and heard nothing from past ApplySilentAfter, on whichever tier it sent -// them (novox/hq ADR 0360 §5, issue 1160): the one rule both the gates between tiers and the walk's end read, through +// them (novox/hq ADR 0360 §7, issue 1160): the one rule both the gates between tiers and the walk's end read, through // walkReports. A machine that reports on its send is not silent from then on. func silentNodes(p inventory.Plan, reports []inventory.Reported, now time.Time, prov inventory.ProvLookup) map[string]bool { _, _, silent := walkReports(p, reports, now, prov) @@ -272,7 +272,7 @@ func silentNodes(p inventory.Plan, reports []inventory.Reported, now time.Time, } // withoutSilent is the machines of running that are not silent: a later tier is not sent to a machine the walk heard -// nothing from, which keeps its want of the plan's builds for the send it gets when it is heard again (ADR 0360 §5). +// nothing from, which keeps its want of the plan's builds for the send it gets when it is heard again (ADR 0360 §7). func withoutSilent(running []string, silent map[string]bool) []string { return slices.DeleteFunc(slices.Clone(running), func(n string) bool { return silent[n] }) } @@ -1129,7 +1129,7 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan, // being behind. Now every module of the tier whose first machine is the same is sent there in one // send, judged there by one gate — kept on the first of them, the others gated by it — and the rest // of the tier's machines are sent once, together, when the gates have passed. - // **A machine silent past the bound is left as it was, on every tier** (novox/hq ADR 0360 §5, issue 1160): the + // **A machine silent past the bound is left as it was, on every tier** (novox/hq ADR 0360 §7, issue 1160): the // walk's end names it, and the gates between tiers and the sends of the tiers after do not wait for it or go to it. allReports, err := inv.LastReports(ctx) if err != nil { @@ -1475,7 +1475,7 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan, return changed, nil } } - // **A walk ends on its machines' reports** (novox/hq ADR 0360 §4, §5): after its last tier, every machine it sent + // **A walk ends on its machines' reports** (novox/hq ADR 0360 §4, §7): after its last tier, every machine it sent // has reported on that send, or is silent past ApplySilentAfter; never a health reading. end := "" if p.Tier == len(p.Tiers)-1 { @@ -1542,7 +1542,7 @@ func walkReports(p inventory.Plan, reports []inventory.Reported, now time.Time, carried[node] = append(carried[node], m) } } - // **Each machine on the walk's last send to it** (issue 1886): a send carries the machine's whole declaration (ADR + // **Each machine on the walk's last send to it** (novox/hq ADR 0531, issue 1886): a send carries the machine's whole declaration (ADR // 0221), so a report on the last one answers every earlier one, and the bound runs from it. Judged on every send // it had made, a walk whose tiers were sent hours apart — R4e, stopped at tier 1 and resumed — held tier 0's send of // 00:23 against tier 1's templates and named its machines silent 35 seconds after tier 1 was sent. diff --git a/cmd/mesh-controller/walk_resumed_silence_1886_test.go b/cmd/mesh-controller/walk_resumed_silence_1886_test.go index 3c0deeaf..bb566c7f 100644 --- a/cmd/mesh-controller/walk_resumed_silence_1886_test.go +++ b/cmd/mesh-controller/walk_resumed_silence_1886_test.go @@ -149,3 +149,25 @@ func TestALostWalkWithATierUnsentIsNotLostToSilence(t *testing.T) { t.Fatal("1886: a plan failed by a build was read as lost to silence") } } + +// A walk lost to silence is refused its resume beside a newer open plan of its repository and branch, which supersedes +// it, and beside a related open plan: one plan at a time (ADR 0531). +func TestRetryRefusesAWalkLostToSilenceBesideANewerOrARelatedOpenPlan(t *testing.T) { + p := r4eLost() + created := time.Date(2026, 10, 22, 22, 15, 9, 0, time.UTC) + p.Repository, p.Branch, p.Commit, p.Created = "novox/mesh-sdk", "main", "7aacbc16", created + newerOpen := inventory.Plan{ID: "plan-newer", Repository: "novox/mesh-sdk", Branch: "main", Commit: "d00dfeed", + Created: created.Add(time.Hour), State: inventory.PlanBuilding, Tiers: [][]string{{"zsh"}}, + Modules: map[string]*inventory.PlanModule{"zsh": {State: "asked"}}} + if err := retryRefusal(p, []inventory.Plan{p, newerOpen}, ownPathOf(nil)); err == nil || + !strings.Contains(err.Error(), "plan-newer supersedes it") { + t.Errorf("1886: a walk lost to silence was resumed under a newer open plan of its branch: %v", err) + } + beside := inventory.Plan{ID: "plan-beside", Repository: "novox/mesh-sdk", Branch: "release", Commit: "bbadbb2a", + Created: created.Add(time.Hour), State: inventory.PlanBuilding, Tiers: [][]string{{"mesh-build-image"}}, + Modules: map[string]*inventory.PlanModule{"mesh-build-image": {State: "asked"}}} + if err := retryRefusal(p, []inventory.Plan{p, beside}, ownPathOf(nil)); err == nil || + !strings.Contains(err.Error(), "one walk at a time") { + t.Errorf("1886: a walk lost to silence was resumed beside a related open plan: %v", err) + } +}