159 lines
3.7 KiB
Go
159 lines
3.7 KiB
Go
package resilience
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestRetryOnSuccess(t *testing.T) {
|
||
|
|
cfg := &RetryConfig{MaxAttempts: 3}
|
||
|
|
attempts := 0
|
||
|
|
|
||
|
|
resp, err := DoRetry(context.Background(), cfg, func(ctx context.Context, attempt int) (*http.Response, error) {
|
||
|
|
attempts++
|
||
|
|
return &http.Response{
|
||
|
|
StatusCode: 200,
|
||
|
|
Body: io.NopCloser(strings.NewReader("ok")),
|
||
|
|
}, nil
|
||
|
|
})
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("unexpected error: %v", err)
|
||
|
|
}
|
||
|
|
if attempts != 1 {
|
||
|
|
t.Errorf("expected 1 attempt on success, got %d", attempts)
|
||
|
|
}
|
||
|
|
if resp.StatusCode != 200 {
|
||
|
|
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
resp.Body.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRetryOn5xx(t *testing.T) {
|
||
|
|
cfg := &RetryConfig{
|
||
|
|
MaxAttempts: 3,
|
||
|
|
InitialBackoff: 10 * time.Millisecond,
|
||
|
|
MaxBackoff: 50 * time.Millisecond,
|
||
|
|
BackoffMultiplier: 2.0,
|
||
|
|
}
|
||
|
|
attempts := 0
|
||
|
|
|
||
|
|
resp, err := DoRetry(context.Background(), cfg, func(ctx context.Context, attempt int) (*http.Response, error) {
|
||
|
|
attempts++
|
||
|
|
if attempt < 2 {
|
||
|
|
// First two attempts return 503
|
||
|
|
return &http.Response{
|
||
|
|
StatusCode: 503,
|
||
|
|
Body: io.NopCloser(strings.NewReader("unavailable")),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
// Third attempt succeeds
|
||
|
|
return &http.Response{
|
||
|
|
StatusCode: 200,
|
||
|
|
Body: io.NopCloser(strings.NewReader("ok")),
|
||
|
|
}, nil
|
||
|
|
})
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("unexpected error: %v", err)
|
||
|
|
}
|
||
|
|
if attempts != 3 {
|
||
|
|
t.Errorf("expected 3 attempts (2 retries), got %d", attempts)
|
||
|
|
}
|
||
|
|
if resp.StatusCode != 200 {
|
||
|
|
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
resp.Body.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRetryExhaustion(t *testing.T) {
|
||
|
|
cfg := &RetryConfig{
|
||
|
|
MaxAttempts: 2,
|
||
|
|
InitialBackoff: 10 * time.Millisecond,
|
||
|
|
MaxBackoff: 50 * time.Millisecond,
|
||
|
|
}
|
||
|
|
attempts := 0
|
||
|
|
|
||
|
|
resp, err := DoRetry(context.Background(), cfg, func(ctx context.Context, attempt int) (*http.Response, error) {
|
||
|
|
attempts++
|
||
|
|
// Always return 503
|
||
|
|
return &http.Response{
|
||
|
|
StatusCode: 503,
|
||
|
|
Body: io.NopCloser(strings.NewReader("unavailable")),
|
||
|
|
}, nil
|
||
|
|
})
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("unexpected error: %v", err)
|
||
|
|
}
|
||
|
|
if attempts != 2 {
|
||
|
|
t.Errorf("expected 2 attempts (max), got %d", attempts)
|
||
|
|
}
|
||
|
|
if resp.StatusCode != 503 {
|
||
|
|
t.Errorf("expected status 503, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
resp.Body.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRetryWithContext(t *testing.T) {
|
||
|
|
cfg := &RetryConfig{MaxAttempts: 10}
|
||
|
|
attempts := 0
|
||
|
|
|
||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
// Cancel after a short delay
|
||
|
|
go func() {
|
||
|
|
time.Sleep(50 * time.Millisecond)
|
||
|
|
cancel()
|
||
|
|
}()
|
||
|
|
|
||
|
|
resp, err := DoRetry(ctx, cfg, func(ctx context.Context, attempt int) (*http.Response, error) {
|
||
|
|
attempts++
|
||
|
|
time.Sleep(30 * time.Millisecond)
|
||
|
|
return &http.Response{
|
||
|
|
StatusCode: 503,
|
||
|
|
Body: io.NopCloser(strings.NewReader("unavailable")),
|
||
|
|
}, nil
|
||
|
|
})
|
||
|
|
|
||
|
|
if err != context.Canceled {
|
||
|
|
t.Errorf("expected context.Canceled error, got: %v", err)
|
||
|
|
}
|
||
|
|
if resp != nil {
|
||
|
|
resp.Body.Close()
|
||
|
|
}
|
||
|
|
// Should have fewer than all attempts due to cancellation
|
||
|
|
if attempts >= 10 {
|
||
|
|
t.Errorf("expected fewer than 10 attempts due to cancellation, got %d", attempts)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRetryPolicyShouldRetry(t *testing.T) {
|
||
|
|
policy := &RetryPolicy{Retryable: true}
|
||
|
|
|
||
|
|
resp503 := &http.Response{StatusCode: 503}
|
||
|
|
if !policy.ShouldRetry(resp503) {
|
||
|
|
t.Errorf("expected to retry on 503")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp200 := &http.Response{StatusCode: 200}
|
||
|
|
if policy.ShouldRetry(resp200) {
|
||
|
|
t.Errorf("expected not to retry on 200")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp404 := &http.Response{StatusCode: 404}
|
||
|
|
if policy.ShouldRetry(resp404) {
|
||
|
|
t.Errorf("expected not to retry on 404")
|
||
|
|
}
|
||
|
|
|
||
|
|
policyNoRetry := &RetryPolicy{Retryable: false}
|
||
|
|
if policyNoRetry.ShouldRetry(resp503) {
|
||
|
|
t.Errorf("expected not to retry when retryable=false")
|
||
|
|
}
|
||
|
|
}
|