Implements retry middleware as a RoundTripper wrapper: - Exponential and constant backoff strategies with jitter - RFC 7231 Retry-After header parsing (seconds and HTTP-date) - Default policy retries idempotent methods on 429/5xx and network errors - Body restoration via GetBody, context cancellation, response body cleanup
78 lines
2.1 KiB
Go
78 lines
2.1 KiB
Go
package retry
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestExponentialBackoff(t *testing.T) {
|
|
t.Run("doubles each attempt", func(t *testing.T) {
|
|
b := ExponentialBackoff(100*time.Millisecond, 10*time.Second, false)
|
|
|
|
want := []time.Duration{
|
|
100 * time.Millisecond, // attempt 0: base
|
|
200 * time.Millisecond, // attempt 1: base*2
|
|
400 * time.Millisecond, // attempt 2: base*4
|
|
800 * time.Millisecond, // attempt 3: base*8
|
|
1600 * time.Millisecond, // attempt 4: base*16
|
|
}
|
|
|
|
for i, expected := range want {
|
|
got := b.Delay(i)
|
|
if got != expected {
|
|
t.Errorf("attempt %d: expected %v, got %v", i, expected, got)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("caps at max", func(t *testing.T) {
|
|
b := ExponentialBackoff(100*time.Millisecond, 500*time.Millisecond, false)
|
|
|
|
// attempt 0: 100ms, 1: 200ms, 2: 400ms, 3: 500ms (capped), 4: 500ms
|
|
for _, attempt := range []int{3, 4, 10} {
|
|
got := b.Delay(attempt)
|
|
if got != 500*time.Millisecond {
|
|
t.Errorf("attempt %d: expected cap at 500ms, got %v", attempt, got)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("with jitter adds randomness", func(t *testing.T) {
|
|
base := 100 * time.Millisecond
|
|
b := ExponentialBackoff(base, 10*time.Second, true)
|
|
|
|
// Run multiple times; with jitter, delay >= base for attempt 0.
|
|
// Also verify not all values are identical (randomness).
|
|
seen := make(map[time.Duration]bool)
|
|
for range 20 {
|
|
d := b.Delay(0)
|
|
if d < base {
|
|
t.Fatalf("delay %v is less than base %v", d, base)
|
|
}
|
|
// With jitter: delay = base + rand in [0, base/2), so max is base*1.5
|
|
maxExpected := base + base/2
|
|
if d > maxExpected {
|
|
t.Fatalf("delay %v exceeds expected max %v", d, maxExpected)
|
|
}
|
|
seen[d] = true
|
|
}
|
|
if len(seen) < 2 {
|
|
t.Errorf("expected jitter to produce varying delays, got %d unique values", len(seen))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConstantBackoff(t *testing.T) {
|
|
t.Run("always returns same value", func(t *testing.T) {
|
|
d := 250 * time.Millisecond
|
|
b := ConstantBackoff(d)
|
|
|
|
for _, attempt := range []int{0, 1, 2, 5, 100} {
|
|
got := b.Delay(attempt)
|
|
if got != d {
|
|
t.Errorf("attempt %d: expected %v, got %v", attempt, d, got)
|
|
}
|
|
}
|
|
})
|
|
}
|