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
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:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user