package recovery import ( "context" "errors" "fmt" "math" "math/rand" "time" "go.uber.org/zap" ) // RetryConfig configure le comportement du retry (BE-SVC-021) type RetryConfig struct { MaxAttempts int // Nombre maximum de tentatives InitialDelay time.Duration // Délai initial avant le premier retry MaxDelay time.Duration // Délai maximum entre les tentatives Multiplier float64 // Multiplicateur pour le backoff exponentiel Jitter bool // Ajouter du jitter pour éviter le thundering herd RetryableErrors []error // Liste des erreurs qui doivent être retryées RetryableFunc func(error) bool // Fonction pour déterminer si une erreur est retryable OnRetry func(attempt int, err error) // Callback appelé à chaque retry } // DefaultRetryConfig retourne une configuration de retry par défaut func DefaultRetryConfig() *RetryConfig { return &RetryConfig{ MaxAttempts: 3, InitialDelay: 100 * time.Millisecond, MaxDelay: 5 * time.Second, Multiplier: 2.0, Jitter: true, } } // RetryStrategy définit la stratégie de retry type RetryStrategy int const ( // ExponentialBackoff utilise un backoff exponentiel ExponentialBackoff RetryStrategy = iota // LinearBackoff utilise un backoff linéaire LinearBackoff // FixedBackoff utilise un délai fixe FixedBackoff ) // Retry exécute une fonction avec retry automatique (BE-SVC-021) func Retry(ctx context.Context, fn func() error, config *RetryConfig) error { if config == nil { config = DefaultRetryConfig() } var lastErr error for attempt := 1; attempt <= config.MaxAttempts; attempt++ { // Vérifier si le contexte est annulé avant d'exécuter select { case <-ctx.Done(): return fmt.Errorf("context cancelled: %w", ctx.Err()) default: } // Exécuter la fonction err := fn() if err == nil { return nil // Succès } lastErr = err // Vérifier si le contexte est annulé après l'exécution select { case <-ctx.Done(): return fmt.Errorf("context cancelled: %w", ctx.Err()) default: } // Vérifier si l'erreur est retryable if !isRetryable(err, config) { return err // Erreur non retryable, arrêter } // Si c'est la dernière tentative, ne pas attendre if attempt >= config.MaxAttempts { break } // Calculer le délai avant le prochain retry delay := calculateDelay(attempt, config) // Appeler le callback OnRetry si défini if config.OnRetry != nil { config.OnRetry(attempt, err) } // Attendre avant le prochain retry select { case <-ctx.Done(): return fmt.Errorf("context cancelled during retry: %w", ctx.Err()) case <-time.After(delay): // Continuer avec le prochain retry } } return fmt.Errorf("max attempts (%d) reached: %w", config.MaxAttempts, lastErr) } // RetryWithResult exécute une fonction qui retourne un résultat avec retry func RetryWithResult[T any](ctx context.Context, fn func() (T, error), config *RetryConfig) (T, error) { var zero T if config == nil { config = DefaultRetryConfig() } var lastErr error for attempt := 1; attempt <= config.MaxAttempts; attempt++ { select { case <-ctx.Done(): return zero, fmt.Errorf("context cancelled: %w", ctx.Err()) default: } result, err := fn() if err == nil { return result, nil } lastErr = err if !isRetryable(err, config) { return zero, err } if attempt >= config.MaxAttempts { break } delay := calculateDelay(attempt, config) if config.OnRetry != nil { config.OnRetry(attempt, err) } select { case <-ctx.Done(): return zero, fmt.Errorf("context cancelled during retry: %w", ctx.Err()) case <-time.After(delay): } } return zero, fmt.Errorf("max attempts (%d) reached: %w", config.MaxAttempts, lastErr) } // isRetryable détermine si une erreur doit être retryée func isRetryable(err error, config *RetryConfig) bool { if err == nil { return false } // Si une fonction de vérification est fournie, l'utiliser if config.RetryableFunc != nil { return config.RetryableFunc(err) } // Vérifier dans la liste des erreurs retryables if len(config.RetryableErrors) > 0 { for _, retryableErr := range config.RetryableErrors { if errors.Is(err, retryableErr) { return true } } } // Par défaut, retryer toutes les erreurs return true } // calculateDelay calcule le délai avant le prochain retry func calculateDelay(attempt int, config *RetryConfig) time.Duration { var delay time.Duration // Calculer le délai de base selon la stratégie baseDelay := config.InitialDelay if config.Multiplier > 0 { // Backoff exponentiel baseDelay = time.Duration(float64(config.InitialDelay) * math.Pow(config.Multiplier, float64(attempt-1))) } else { // Backoff linéaire baseDelay = config.InitialDelay * time.Duration(attempt) } delay = baseDelay // Appliquer le jitter si activé if config.Jitter { // Jitter aléatoire entre 0 et 25% du délai jitter := time.Duration(rand.Float64() * 0.25 * float64(delay)) delay = delay + jitter } // Limiter au délai maximum if delay > config.MaxDelay { delay = config.MaxDelay } return delay } // RetryWithLogger exécute une fonction avec retry et logging func RetryWithLogger(ctx context.Context, logger *zap.Logger, fn func() error, config *RetryConfig) error { if config == nil { config = DefaultRetryConfig() } // Configurer le callback OnRetry pour logger originalOnRetry := config.OnRetry config.OnRetry = func(attempt int, err error) { if logger != nil { logger.Warn("Retry attempt", zap.Int("attempt", attempt), zap.Int("max_attempts", config.MaxAttempts), zap.Error(err), ) } if originalOnRetry != nil { originalOnRetry(attempt, err) } } return Retry(ctx, fn, config) } // IsRetryableError vérifie si une erreur est généralement retryable // Cette fonction peut être utilisée comme RetryableFunc dans RetryConfig func IsRetryableError(err error) bool { if err == nil { return false } errStr := err.Error() // Erreurs réseau généralement retryables retryablePatterns := []string{ "timeout", "connection refused", "connection reset", "no such host", "network is unreachable", "temporary failure", "server error", "service unavailable", "bad gateway", "gateway timeout", } for _, pattern := range retryablePatterns { if contains(errStr, pattern) { return true } } return false } // contains vérifie si une chaîne contient une sous-chaîne (case-insensitive) func contains(s, substr string) bool { if len(substr) > len(s) { return false } for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }