Let a retry see every plan made after the failed one, and pin the 7-day end of plan-failed (issue 1328, ADR 0451)
mesh/merge-gate head 7f23c537cced merged into trunk 1f708e2d8d72 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 7f23c537cced merged into trunk 1f708e2d8d72 — pass: its merge-check.sh passed
mesh/delivery delivered

This commit is contained in:
2026-10-19 21:48:41 +00:00
parent f1775281e2
commit 7f23c537cc
2 changed files with 112 additions and 6 deletions
+85
View File
@@ -1,6 +1,9 @@
package main package main
import ( import (
"context"
"fmt"
"sort"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -251,3 +254,85 @@ func TestAPassedFailedPlanHasNoActionAndSaysItWasPassed(t *testing.T) {
t.Errorf("not plain: %s\n%+v", why, words) t.Errorf("not plain: %s\n%+v", why, words)
} }
} }
// plansBehind serves plans as the inventory does: the open ones, the most recent n, and every one made since.
type plansBehind []inventory.Plan
func (s plansBehind) OpenPlans(context.Context) ([]inventory.Plan, error) {
var out []inventory.Plan
for _, p := range s {
if p.Open() {
out = append(out, p)
}
}
return out, nil
}
func (s plansBehind) RecentPlans(_ context.Context, limit int) ([]inventory.Plan, error) {
all := append([]inventory.Plan(nil), s...)
sort.SliceStable(all, func(i, j int) bool { return all[i].Created.After(all[j].Created) })
if len(all) > limit {
all = all[:limit]
}
return all, nil
}
func (s plansBehind) PlansSince(_ context.Context, since time.Time) ([]inventory.Plan, error) {
var out []inventory.Plan
for _, p := range s {
if p.Created.After(since) || p.Open() {
out = append(out, p)
}
}
return out, nil
}
// 1328, review round 3: the plan that passed a failed one is seen by `plans retry` however many plans were made after it.
func TestARetrySeesThePassingPlanBeyondTheFiftyMostRecent(t *testing.T) {
failed := aFailedPlan("plan-1", func(p *inventory.Plan) { p.Branch, p.Created = "main", time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC) })
all := plansBehind{failed, laterPlan("plan-done", "main", inventory.PlanDone, failed)}
for i := 0; i < 60; i++ { // 60 newer plans of other repositories push the passing one out of the newest 50
other := laterPlan(fmt.Sprintf("plan-x%d", i), "main", inventory.PlanFailed, failed)
other.Repository = "novox/other"
other.Created = failed.Created.Add(time.Duration(48+i) * time.Hour)
all = append(all, other)
}
plans, err := plansForRetry(t.Context(), all, failed)
if err != nil {
t.Fatal(err)
}
if err := retryRefusal(failed, plans, map[string]string{}); err == nil || !strings.Contains(err.Error(), "plan-done") {
t.Fatalf("the retry was not refused naming the passing plan beyond the 50 most recent: %v", err)
}
}
// ADR 0451 §3: a failed plan that last changed 8 days ago raises nothing, and a raised condition ends once its
// plan's last change passes 7 days.
func TestAFailedPlanLastChangedEightDaysAgoRaisesNothingAndARaisedOneEnds(t *testing.T) {
now := time.Date(2026, 10, 19, 18, 30, 0, 0, time.UTC)
old := aFailedPlan("plan-1", func(p *inventory.Plan) { p.Updated = now.Add(-8 * 24 * time.Hour) })
f := calm(now)
f.failed = []failedPlanFacts{failedFactsFor(old)}
if got := watchPlansFailed(f); len(got) != 0 {
t.Fatalf("a plan last changed 8 days ago raised %+v", got)
}
store := conditions.NewInMemory()
k := conditions.NewKeeper(t.Context(), conditions.Options{Store: store, History: store,
Teller: &conditions.Told{}, Now: func() time.Time { return now }})
defer k.Close(t.Context())
fresh := aFailedPlan("plan-1", func(p *inventory.Plan) { p.Updated = now.Add(-6 * 24 * time.Hour) })
f.failed = []failedPlanFacts{failedFactsFor(fresh)}
if err := k.Reconcile(t.Context(), "S23", watchPlansFailed(f)); err != nil {
t.Fatal(err)
}
if open, _ := k.Open(t.Context()); len(open) != 1 {
t.Fatalf("a plan last changed 6 days ago is not raised: %+v", open)
}
f.now = now.Add(2 * 24 * time.Hour) // its last change is now 8 days old
if err := k.Reconcile(t.Context(), "S23", watchPlansFailed(f)); err != nil {
t.Fatal(err)
}
if open, _ := k.Open(t.Context()); len(open) != 0 {
t.Fatalf("the condition outlived the 7 days: %+v", open)
}
}
+27 -6
View File
@@ -305,6 +305,32 @@ func goesOnAgain(p *inventory.Plan) {
} }
} }
// planReader is what a retry reads the mesh's plans through.
type planReader interface {
OpenPlans(ctx context.Context) ([]inventory.Plan, error)
RecentPlans(ctx context.Context, limit int) ([]inventory.Plan, error)
PlansSince(ctx context.Context, since time.Time) ([]inventory.Plan, error)
}
// plansForRetry are the plans a retry of p judges itself against.
func plansForRetry(ctx context.Context, inv planReader, p inventory.Plan) ([]inventory.Plan, error) {
plans, err := inv.OpenPlans(ctx)
if err != nil {
return nil, err
}
recent, err := inv.RecentPlans(ctx, 50)
if err != nil {
return nil, err
}
// **Every plan made after this one** (novox/hq ADR 0451 §1): a plan that passed it is seen however many plans
// were made since, not only when it is among the newest.
later, err := inv.PlansSince(ctx, p.Created)
if err != nil {
return nil, err
}
return append(append(plans, recent...), later...), nil
}
// retryPlan asks a failed plan's failed modules again, under new ids, and sets it building again at // retryPlan asks a failed plan's failed modules again, under new ids, and sets it building again at
// that tier: what follows is the plan going on as if they had built the first time. // that tier: what follows is the plan going on as if they had built the first time.
func retryPlan(ctx context.Context, open *stores, id string) (string, error) { func retryPlan(ctx context.Context, open *stores, id string) (string, error) {
@@ -318,15 +344,10 @@ func retryPlan(ctx context.Context, open *stores, id string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
plans, err := inv.OpenPlans(ctx) plans, err := plansForRetry(ctx, inv, p)
if err != nil { if err != nil {
return "", err return "", err
} }
recent, err := inv.RecentPlans(ctx, 50)
if err != nil {
return "", err
}
plans = append(plans, recent...)
// **A failed walk whose earlier merges are walked alone is not retried** (novox/hq ADR 0276): the search for // **A failed walk whose earlier merges are walked alone is not retried** (novox/hq ADR 0276): the search for
// the merge that brought the failure answers them now, and a retried walk would name them twice. // the merge that brought the failure answers them now, and a retried walk would name them twice.
// **A walk stopped without some merges is not retried** (novox/hq ADR 0299): the merges it left out were stopped, // **A walk stopped without some merges is not retried** (novox/hq ADR 0299): the merges it left out were stopped,