package spindle
import (
"context"
"slices"
"testing"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/api/org_tangled"
"tangled.org/core/spindle/db"
"tangled.org/core/tapc"
)
func pendingRetries(t *testing.T, d *db.DB) []syntax.DID {
t.Helper()
due, err := d.DueRepoRetries(time.Now().Add(24 * time.Hour))
if err != nil {
t.Fatal(err)
}
return due
}
func TestRepoRetriesBackOffAndGiveUp(t *testing.T) {
d, _ := newTestSpindleDB(t)
now := time.Unix(1000, 0)
r := &repoRetries{now: func() time.Time { return now }}
dueNow := func() []syntax.DID {
t.Helper()
due, err := d.DueRepoRetries(r.clock())
if err != nil {
t.Fatal(err)
}
return due
}
r.fail(d, testRepoDid)
if got := dueNow(); len(got) != 0 {
t.Fatalf("due right after failing: %v", got)
}
now = now.Add(repoRetryBase)
if got := dueNow(); len(got) != 1 || got[0] != testRepoDid {
t.Fatalf("due after the first backoff = %v", got)
}
r.fail(d, testRepoDid)
now = now.Add(repoRetryBase)
if got := dueNow(); len(got) != 0 {
t.Fatalf("second failure should wait twice as long, due = %v", got)
}
dropped := false
for range repoRetryAttempts {
if retained, _ := r.fail(d, testRepoDid); !retained {
dropped = true
break
}
}
if left := pendingRetries(t, d); !dropped || len(left) != 0 {
t.Fatalf("a repo that keeps failing is never dropped, pending = %v", left)
}
}
func TestRepoRetriesSurviveARestart(t *testing.T) {
d, _ := newTestSpindleDB(t)
now := time.Unix(1000, 0)
r := &repoRetries{now: func() time.Time { return now }}
r.fail(d, testRepoDid)
r.fail(d, testRepoDid)
now = now.Add(2 * repoRetryMax)
restarted := &repoRetries{now: func() time.Time { return now }}
if due, err := d.DueRepoRetries(restarted.clock()); err != nil || len(due) != 1 || due[0] != testRepoDid {
t.Fatalf("due after a restart = %v, %v, want the repo that was failing", due, err)
}
restarted.fail(d, testRepoDid)
if attempts, err := d.RepoRetryAttempts(testRepoDid); err != nil || attempts != 3 {
t.Fatalf("attempts after a restart = %d, %v, want the count to carry on at 3", attempts, err)
}
}
func TestAFailedLiveRefreshIsRetriedUntilItWorks(t *testing.T) {
env := newEnsureEnv(t)
env.serve("repo")
env.ensure(t)
env.knot.failing = true
evt := recordEvent(testRepoDid, org_tangled.RepoManifestNSID, "self", tapc.RecordUpdateAction, org_tangled.RepoManifest{})
if err := env.s.processEvent(context.Background(), evt); err == nil {
t.Fatal("processEvent hid a knot failure")
}
if left := pendingRetries(t, env.s.db); !slices.Equal(left, []syntax.DID{testRepoDid}) {
t.Fatalf("retry set = %v, want the repo whose refresh failed", left)
}
env.knot.failing = false
if err := env.s.RefreshRepo(context.Background(), testRepoDid); err != nil {
t.Fatal(err)
}
if left := pendingRetries(t, env.s.db); len(left) != 0 {
t.Fatalf("a successful refresh left %v pending", left)
}
}
func TestAFailedBackfillRefreshIsNotRetriedHere(t *testing.T) {
env := newEnsureEnv(t)
env.serve("repo")
env.ensure(t)
env.knot.failing = true
evt := recordEvent(testRepoDid, org_tangled.RepoManifestNSID, "self", tapc.RecordUpdateAction, org_tangled.RepoManifest{})
evt.Record.Live = false
if err := env.s.processEvent(context.Background(), evt); err == nil {
t.Fatal("processEvent hid a knot failure")
}
if left := pendingRetries(t, env.s.db); len(left) != 0 {
t.Fatalf("backfill parks its own failures, pending = %v", left)
}
}