Ask a failed delivery's Retry on the phone, tell a refused answer, and pass a seat to its only claimant (ADR 0449, ADR 0451) #402
@@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -251,3 +254,85 @@ func TestAPassedFailedPlanHasNoActionAndSaysItWasPassed(t *testing.T) {
|
||||
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
|
||||
// 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) {
|
||||
@@ -318,15 +344,10 @@ func retryPlan(ctx context.Context, open *stores, id string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
plans, err := inv.OpenPlans(ctx)
|
||||
plans, err := plansForRetry(ctx, inv, p)
|
||||
if err != nil {
|
||||
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
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user