Files

154 lines
3.1 KiB
Go
Raw Permalink Normal View History

//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)
}