Add retry transport with configurable backoff and Retry-After support
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
This commit is contained in:
77
retry/backoff_test.go
Normal file
77
retry/backoff_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user