| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- package retry
- import (
- "context"
- "errors"
- "testing"
- "time"
- )
- func TestRunSucceedsOnFirstTry(t *testing.T) {
- calls := 0
- res := Run(context.Background(), Default(), func(ctx context.Context, attempt int) error {
- calls++
- return nil
- })
- if calls != 1 {
- t.Fatalf("want 1 call, got %d", calls)
- }
- if res.Attempts != 1 {
- t.Fatalf("want Attempts=1, got %d", res.Attempts)
- }
- if res.LastError != nil {
- t.Fatalf("want nil err, got %v", res.LastError)
- }
- }
- func TestRunRetriesThenSucceeds(t *testing.T) {
- calls := 0
- cfg := Config{
- MaxAttempts: 5,
- BaseDelay: 1 * time.Millisecond,
- MaxDelay: 5 * time.Millisecond,
- Budget: 5 * time.Second,
- }
- res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
- calls++
- if attempt < 3 {
- return errors.New("transient")
- }
- return nil
- })
- if calls != 3 {
- t.Fatalf("want 3 calls, got %d", calls)
- }
- if res.Attempts != 3 {
- t.Fatalf("want Attempts=3, got %d", res.Attempts)
- }
- if res.LastError != nil {
- t.Fatalf("want nil err, got %v", res.LastError)
- }
- }
- func TestRunExhaustsAttempts(t *testing.T) {
- calls := 0
- cfg := Config{
- MaxAttempts: 3,
- BaseDelay: 1 * time.Millisecond,
- MaxDelay: 5 * time.Millisecond,
- Budget: 5 * time.Second,
- }
- res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
- calls++
- return errors.New("nope")
- })
- if calls != 3 {
- t.Fatalf("want 3 calls, got %d", calls)
- }
- if res.Attempts != 3 {
- t.Fatalf("want Attempts=3, got %d", res.Attempts)
- }
- if res.LastError == nil {
- t.Fatalf("want non-nil err")
- }
- }
- func TestRunShortCircuitsOnPermanent(t *testing.T) {
- calls := 0
- cfg := Config{
- MaxAttempts: 5,
- BaseDelay: 1 * time.Millisecond,
- MaxDelay: 5 * time.Millisecond,
- Budget: 5 * time.Second,
- }
- res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
- calls++
- return &PermanentError{Err: errors.New("bad token")}
- })
- if calls != 1 {
- t.Fatalf("want 1 call, got %d", calls)
- }
- if !IsPermanent(res.LastError) {
- t.Fatalf("want PermanentError, got %T: %v", res.LastError, res.LastError)
- }
- }
- func TestRunRespectsBudget(t *testing.T) {
- calls := 0
- cfg := Config{
- MaxAttempts: 100,
- BaseDelay: 50 * time.Millisecond,
- MaxDelay: 200 * time.Millisecond,
- Budget: 100 * time.Millisecond, // tight budget
- }
- start := time.Now()
- res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
- calls++
- return errors.New("nope")
- })
- elapsed := time.Since(start)
- if elapsed > 500*time.Millisecond {
- t.Fatalf("budget exceeded: took %s", elapsed)
- }
- // With Budget=100ms and base 50ms, we expect at most
- // 3-4 calls (50 + 100 = 150ms would already bust the
- // budget for the next wait).
- if calls > 5 {
- t.Fatalf("too many calls under tight budget: %d", calls)
- }
- if res.LastError == nil {
- t.Fatalf("want non-nil err")
- }
- }
- func TestRunRespectsContextCancel(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- calls := 0
- cfg := Config{
- MaxAttempts: 10,
- BaseDelay: 100 * time.Millisecond,
- MaxDelay: 500 * time.Millisecond,
- Budget: 5 * time.Second,
- }
- // Cancel the ctx after the first call.
- res := Run(ctx, cfg, func(ctx context.Context, attempt int) error {
- calls++
- if calls == 1 {
- // Cancel while we're sleeping.
- go func() {
- time.Sleep(20 * time.Millisecond)
- cancel()
- }()
- }
- return errors.New("nope")
- })
- if calls >= 10 {
- t.Fatalf("ctx cancel did not stop the loop; calls=%d", calls)
- }
- if res.LastError == nil {
- t.Fatalf("want non-nil err")
- }
- }
- func TestBackoffMonotonic(t *testing.T) {
- // Per-attempt wait is the time we sleep BEFORE that
- // attempt. So attempt 1 = no wait, attempt 2 = base,
- // attempt 3 = base*2, etc., capped at max.
- base := 100 * time.Millisecond
- max := 2 * time.Second
- prev := time.Duration(0)
- for n := 2; n <= 10; n++ {
- got := backoff(base, max, n)
- if n == 2 && got != base {
- t.Fatalf("attempt 2: want %s, got %s", base, got)
- }
- if got > max {
- t.Fatalf("attempt %d: exceeded max: %s", n, got)
- }
- // Wait must be >= previous (or equal under cap).
- if got < prev {
- t.Fatalf("attempt %d: wait decreased: %s < %s", n, got, prev)
- }
- prev = got
- }
- }
|