154 lines
3.1 KiB
Go
154 lines
3.1 KiB
Go
//go:build linux || freebsd
|
|
// +build linux freebsd
|
|
|
|
package transport
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// RetryConfig configures the retry logic
|
|
type RetryConfig struct {
|
|
// Maximum number of attempts
|
|
MaxRetries int
|
|
|
|
// Initial delay between attempts
|
|
InitialDelay time.Duration
|
|
|
|
// Maximum delay between attempts
|
|
MaxDelay time.Duration
|
|
|
|
// Multiplier for exponential backoff
|
|
Multiplier float64
|
|
|
|
// Retry on these HTTP status codes
|
|
RetryableStatusCodes []int
|
|
|
|
// Callback before each retry
|
|
OnRetry func(attempt int, err error, delay time.Duration)
|
|
}
|
|
|
|
// DefaultRetryConfig returns the default configuration
|
|
func DefaultRetryConfig() *RetryConfig {
|
|
return &RetryConfig{
|
|
MaxRetries: 3,
|
|
InitialDelay: 1 * time.Second,
|
|
MaxDelay: 30 * time.Second,
|
|
Multiplier: 2.0,
|
|
RetryableStatusCodes: []int{408, 429, 500, 502, 503, 504},
|
|
}
|
|
}
|
|
|
|
// RetryFunc is a function that can be retried
|
|
type RetryFunc func(ctx context.Context, attempt int) error
|
|
|
|
// Retry performs an operation with retry logic
|
|
func Retry(ctx context.Context, fn RetryFunc, cfg *RetryConfig) error {
|
|
if cfg == nil {
|
|
cfg = DefaultRetryConfig()
|
|
}
|
|
|
|
var lastErr error
|
|
delay := cfg.InitialDelay
|
|
|
|
for attempt := 1; attempt <= cfg.MaxRetries+1; attempt++ {
|
|
// Check the context
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
|
|
// Perform the operation
|
|
err := fn(ctx, attempt)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
lastErr = err
|
|
|
|
// Last attempt - return error
|
|
if attempt > cfg.MaxRetries {
|
|
return fmt.Errorf("all %d attempts failed: %w", cfg.MaxRetries+1, err)
|
|
}
|
|
|
|
// Callback before retry
|
|
if cfg.OnRetry != nil {
|
|
cfg.OnRetry(attempt, err, delay)
|
|
}
|
|
|
|
// Wait before the next attempt
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(delay):
|
|
// Exponential backoff
|
|
delay = time.Duration(float64(delay) * cfg.Multiplier)
|
|
if delay > cfg.MaxDelay {
|
|
delay = cfg.MaxDelay
|
|
}
|
|
}
|
|
}
|
|
|
|
return lastErr
|
|
}
|
|
|
|
// IsRetryableError determines whether the error is retryable
|
|
func IsRetryableError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
errStr := err.Error()
|
|
|
|
// Network errors
|
|
retryableErrors := []string{
|
|
"connection refused",
|
|
"connection reset",
|
|
"connection timed out",
|
|
"i/o timeout",
|
|
"temporary failure",
|
|
"no route to host",
|
|
}
|
|
|
|
for _, re := range retryableErrors {
|
|
if containsIgnoreCase(errStr, re) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsRetryableStatusCode determines whether the HTTP status is retryable
|
|
func IsRetryableStatusCode(status int, cfg *RetryConfig) bool {
|
|
if cfg == nil {
|
|
cfg = DefaultRetryConfig()
|
|
}
|
|
|
|
for _, code := range cfg.RetryableStatusCodes {
|
|
if status == code {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// WithMaxRetries creates a RetryConfig with the specified maximum retries
|
|
func WithMaxRetries(maxRetries int) *RetryConfig {
|
|
cfg := DefaultRetryConfig()
|
|
if maxRetries > 0 {
|
|
cfg.MaxRetries = maxRetries
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// containsIgnoreCase helper function
|
|
func containsIgnoreCase(s, substr string) bool {
|
|
s = strings.ToLower(s)
|
|
substr = strings.ToLower(substr)
|
|
return strings.Contains(s, substr)
|
|
}
|