feat: phase 8 serviceadapter crd rollout (32/33 tasks)
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package resilience
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RetryConfig holds retry settings.
|
||||
type RetryConfig struct {
|
||||
// MaxAttempts is the maximum number of attempts (includes initial).
|
||||
MaxAttempts int
|
||||
// InitialBackoff is the initial backoff duration.
|
||||
InitialBackoff time.Duration
|
||||
// MaxBackoff is the maximum backoff duration.
|
||||
MaxBackoff time.Duration
|
||||
// BackoffMultiplier is the exponential backoff multiplier.
|
||||
BackoffMultiplier float64
|
||||
}
|
||||
|
||||
// DefaultRetryConfig provides sensible defaults.
|
||||
func DefaultRetryConfig() *RetryConfig {
|
||||
return &RetryConfig{
|
||||
MaxAttempts: 3,
|
||||
InitialBackoff: 100 * time.Millisecond,
|
||||
MaxBackoff: 2 * time.Second,
|
||||
BackoffMultiplier: 2.0,
|
||||
}
|
||||
}
|
||||
|
||||
// RetryFunc executes a function with blind retry on 5xx.
|
||||
// Returns the response and any error from the function itself (not retry logic).
|
||||
type RetryFunc func(ctx context.Context, attempt int) (*http.Response, error)
|
||||
|
||||
// DoRetry executes the function with exponential backoff on 5xx responses.
|
||||
// Returns the final response (could be 5xx if all retries exhausted) and any error.
|
||||
func DoRetry(ctx context.Context, cfg *RetryConfig, fn RetryFunc) (*http.Response, error) {
|
||||
if cfg == nil {
|
||||
cfg = DefaultRetryConfig()
|
||||
}
|
||||
|
||||
var lastResp *http.Response
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
|
||||
// Check context before attempting
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if lastResp != nil {
|
||||
lastResp.Body.Close()
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
resp, err := fn(ctx, attempt)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
// Don't retry on network errors in the retry loop itself
|
||||
// Let caller decide if those should be retried
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Success (not 5xx)
|
||||
if resp.StatusCode < 500 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 5xx — close and retry
|
||||
if lastResp != nil {
|
||||
lastResp.Body.Close()
|
||||
}
|
||||
lastResp = resp
|
||||
|
||||
// If this was the last attempt, return the 5xx response
|
||||
if attempt == cfg.MaxAttempts-1 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Calculate backoff with jitter
|
||||
backoff := calculateBackoff(attempt, cfg)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
resp.Body.Close()
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(backoff):
|
||||
// Continue to next attempt
|
||||
}
|
||||
}
|
||||
|
||||
return lastResp, lastErr
|
||||
}
|
||||
|
||||
// calculateBackoff computes exponential backoff with jitter.
|
||||
func calculateBackoff(attempt int, cfg *RetryConfig) time.Duration {
|
||||
// Exponential: initial * (multiplier ^ attempt)
|
||||
backoff := time.Duration(float64(cfg.InitialBackoff) * (pow(cfg.BackoffMultiplier, float64(attempt))))
|
||||
|
||||
// Cap at max
|
||||
if backoff > cfg.MaxBackoff {
|
||||
backoff = cfg.MaxBackoff
|
||||
}
|
||||
|
||||
// Add jitter: ±20%
|
||||
jitterRange := backoff / 5
|
||||
if jitterRange <= 0 {
|
||||
return backoff
|
||||
}
|
||||
|
||||
jitter := time.Duration(rand.Int63n(int64(2 * jitterRange)) - int64(jitterRange))
|
||||
|
||||
return backoff + jitter
|
||||
}
|
||||
|
||||
func pow(base, exp float64) float64 {
|
||||
result := 1.0
|
||||
for i := 0; i < int(exp); i++ {
|
||||
result *= base
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RetryPolicy determines whether to retry based on response and config.
|
||||
type RetryPolicy struct {
|
||||
Retryable bool // Whether this adapter allows retries
|
||||
}
|
||||
|
||||
// ShouldRetry determines if a response should be retried.
|
||||
func (p *RetryPolicy) ShouldRetry(resp *http.Response) bool {
|
||||
if !p.Retryable {
|
||||
return false
|
||||
}
|
||||
return resp != nil && resp.StatusCode >= 500
|
||||
}
|
||||
Reference in New Issue
Block a user