Pin the resume of a lost walk's refusals beside a newer or a related plan, and cite ADR 0360 §7 and ADR 0531 (issue 1886)
mesh/merge-gate head 4d4d51eb017f merged into trunk 219fdcd7f00c — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; the ch…
mesh/repo-check head 4d4d51eb017f merged into trunk 219fdcd7f00c — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/check failed: integration: github.com/novox/mesh-controller/internal/identity.TestACertificateChainsToTheMeshsOwnAuthority: authority_test.go:45:…

This commit is contained in:
2026-10-23 08:10:08 +02:00
parent b7d996878b
commit 4d4d51eb01
3 changed files with 32 additions and 10 deletions
+5 -5
View File
@@ -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) {
+5 -5
View File
@@ -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.
@@ -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)
}
}