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