package retry import ( "testing" "time" ) // ============================================================ // ExponentialBackoff 测试 // ============================================================ func TestExponentialBackoff_BasicRetry(t *testing.T) { b := DefaultExponentialBackoff() err := &mockRetryError{ category: "server_error", retryable: true, retryInfo: &RetryInfo{Retryable: true}, } d := b.ShouldRetry(err, 1, defaultCtx()) if d == nil || !d.Retry { t.Fatal("should retry server error on attempt 1") } // 第一次退避应在 500ms-625ms 范围(500 + 0~25% jitter) if d.Delay < 500*time.Millisecond || d.Delay > 700*time.Millisecond { t.Errorf("attempt 1 delay = %v, expected ~500ms", d.Delay) } } func TestExponentialBackoff_DelayGrows(t *testing.T) { b := DefaultExponentialBackoff() err := &mockRetryError{ category: "server_error", retryable: true, retryInfo: &RetryInfo{Retryable: true}, } d1 := b.ShouldRetry(err, 1, defaultCtx()) d3 := b.ShouldRetry(err, 3, defaultCtx()) d5 := b.ShouldRetry(err, 5, defaultCtx()) if d1.Delay >= d3.Delay || d3.Delay >= d5.Delay { t.Errorf("delays should grow: %v < %v < %v", d1.Delay, d3.Delay, d5.Delay) } } func TestExponentialBackoff_MaxDelayCap(t *testing.T) { b := &ExponentialBackoff{ BaseDelay: 500 * time.Millisecond, MaxDelay: 2 * time.Second, MaxRetries: 20, JitterPct: 0.25, } err := &mockRetryError{ category: "server_error", retryable: true, retryInfo: &RetryInfo{Retryable: true}, } d := b.ShouldRetry(err, 15, defaultCtx()) if d == nil || !d.Retry { t.Fatal("should still retry") } // 不应超过 MaxDelay + jitter maxWithJitter := 2*time.Second + time.Duration(float64(2*time.Second)*0.25) if d.Delay > maxWithJitter { t.Errorf("delay %v exceeds max+jitter %v", d.Delay, maxWithJitter) } } func TestExponentialBackoff_MaxRetriesExceeded(t *testing.T) { b := &ExponentialBackoff{MaxRetries: 3} err := &mockRetryError{ category: "server_error", retryable: true, retryInfo: &RetryInfo{Retryable: true}, } d := b.ShouldRetry(err, 4, defaultCtx()) if d == nil || d.Retry { t.Error("should not retry after max retries exceeded") } } func TestExponentialBackoff_NonRetryableError(t *testing.T) { b := DefaultExponentialBackoff() err := &mockRetryError{ category: "auth_error", retryable: false, retryInfo: &RetryInfo{Retryable: false}, } d := b.ShouldRetry(err, 1, defaultCtx()) if d == nil || d.Retry { t.Error("should not retry non-retryable error") } } func TestExponentialBackoff_UsesServerRetryAfter(t *testing.T) { b := DefaultExponentialBackoff() err := &mockRetryError{ category: "rate_limit", retryable: true, retryInfo: &RetryInfo{Retryable: true, After: 30 * time.Second}, } d := b.ShouldRetry(err, 1, defaultCtx()) if d == nil || !d.Retry { t.Fatal("should retry") } if d.Delay != 30*time.Second { t.Errorf("should use server retry-after: got %v", d.Delay) } } func TestExponentialBackoff_MaxDurationExceeded(t *testing.T) { b := DefaultExponentialBackoff() err := &mockRetryError{ category: "server_error", retryable: true, retryInfo: &RetryInfo{Retryable: true}, } ctx := &RetryContext{ IsForeground: true, StartTime: time.Now().Add(-2 * time.Hour), MaxDuration: 1 * time.Hour, ConsecutiveCounts: make(map[string]int), } d := b.ShouldRetry(err, 1, ctx) if d == nil || d.Retry { t.Error("should not retry after max duration exceeded") } } // ============================================================ // ForegroundOnly 测试 // ============================================================ func TestForegroundOnly_BackgroundDropped(t *testing.T) { f := &ForegroundOnly{} err := makeErr("server_overload") ctx := &RetryContext{IsForeground: false, ConsecutiveCounts: make(map[string]int)} d := f.ShouldRetry(err, 1, ctx) if d == nil || d.Retry { t.Error("background overload should be dropped") } } func TestForegroundOnly_ForegroundPasses(t *testing.T) { f := &ForegroundOnly{} err := makeErr("server_overload") ctx := &RetryContext{IsForeground: true, ConsecutiveCounts: make(map[string]int)} d := f.ShouldRetry(err, 1, ctx) if d != nil { t.Error("foreground should pass through (nil = no opinion)") } } func TestForegroundOnly_NonOverloadPasses(t *testing.T) { f := &ForegroundOnly{} err := makeErr("rate_limit") ctx := &RetryContext{IsForeground: false, ConsecutiveCounts: make(map[string]int)} d := f.ShouldRetry(err, 1, ctx) if d != nil { t.Error("non-overload errors should pass through") } } // ============================================================ // ConsecutiveLimit 测试 // ============================================================ func TestConsecutiveLimit_BelowLimit(t *testing.T) { c := &ConsecutiveLimit{Category: "server_overload", Limit: 3} err := makeErr("server_overload") ctx := &RetryContext{ConsecutiveCounts: map[string]int{"server_overload": 2}} d := c.ShouldRetry(err, 1, ctx) if d != nil { t.Error("below limit should pass through") } } func TestConsecutiveLimit_AtLimit(t *testing.T) { c := &ConsecutiveLimit{Category: "server_overload", Limit: 3} err := makeErr("server_overload") ctx := &RetryContext{ConsecutiveCounts: map[string]int{"server_overload": 3}} d := c.ShouldRetry(err, 1, ctx) if d == nil || d.Retry { t.Error("at limit should stop retry") } } func TestConsecutiveLimit_WrongCategory(t *testing.T) { c := &ConsecutiveLimit{Category: "server_overload", Limit: 3} err := makeErr("rate_limit") ctx := &RetryContext{ConsecutiveCounts: map[string]int{"rate_limit": 5}} d := c.ShouldRetry(err, 1, ctx) if d != nil { t.Error("wrong category should pass through") } } // ============================================================ // ServerDirective 测试 // ============================================================ func TestServerDirective_True(t *testing.T) { s := &ServerDirective{} v := true err := &mockRetryError{ category: "rate_limit", retryable: true, retryInfo: &RetryInfo{ServerSaid: &v}, } d := s.ShouldRetry(err, 1, defaultCtx()) if d != nil { t.Error("server true should pass through (let backoff decide delay)") } } func TestServerDirective_False(t *testing.T) { s := &ServerDirective{} v := false err := &mockRetryError{ category: "rate_limit", retryable: true, retryInfo: &RetryInfo{ServerSaid: &v}, } d := s.ShouldRetry(err, 1, defaultCtx()) if d == nil || d.Retry { t.Error("server false should stop retry") } } func TestServerDirective_NoOpinion(t *testing.T) { s := &ServerDirective{} err := makeErr("server_error") d := s.ShouldRetry(err, 1, defaultCtx()) if d != nil { t.Error("no server opinion should pass through") } } // ============================================================ // MaxAttemptsLimit 测试 // ============================================================ func TestMaxAttemptsLimit(t *testing.T) { m := &MaxAttemptsLimit{Max: 5} if d := m.ShouldRetry(makeErr("server_error"), 5, defaultCtx()); d != nil { t.Error("at limit should pass through") } if d := m.ShouldRetry(makeErr("server_error"), 6, defaultCtx()); d == nil || d.Retry { t.Error("above limit should stop") } }