package recovery import ( "context" "errors" "fmt" "time" "go.uber.org/zap" ) // ErrorRecoveryStrategy définit une stratégie de récupération d'erreur (BE-SVC-021) type ErrorRecoveryStrategy interface { // Recover tente de récupérer d'une erreur Recover(ctx context.Context, err error) error // CanRecover détermine si cette stratégie peut récupérer de cette erreur CanRecover(err error) bool } // RetryRecoveryStrategy récupère d'une erreur en réessayant l'opération type RetryRecoveryStrategy struct { config *RetryConfig logger *zap.Logger fn func() error } // NewRetryRecoveryStrategy crée une nouvelle stratégie de récupération par retry func NewRetryRecoveryStrategy(fn func() error, config *RetryConfig, logger *zap.Logger) *RetryRecoveryStrategy { if config == nil { config = DefaultRetryConfig() } return &RetryRecoveryStrategy{ config: config, logger: logger, fn: fn, } } // CanRecover vérifie si cette stratégie peut récupérer de l'erreur func (r *RetryRecoveryStrategy) CanRecover(err error) bool { return IsRetryableError(err) } // Recover tente de récupérer en réessayant l'opération func (r *RetryRecoveryStrategy) Recover(ctx context.Context, err error) error { if r.fn == nil { return fmt.Errorf("no recovery function provided") } // La fonction fn sera appelée par Retry, donc on peut l'utiliser directement // Retry va appeler fn() plusieurs fois jusqu'à succès ou max attempts return RetryWithLogger(ctx, r.logger, r.fn, r.config) } // FallbackRecoveryStrategy récupère d'une erreur en utilisant une fonction de fallback type FallbackRecoveryStrategy struct { fallbackFn func() error logger *zap.Logger } // NewFallbackRecoveryStrategy crée une nouvelle stratégie de récupération par fallback func NewFallbackRecoveryStrategy(fallbackFn func() error, logger *zap.Logger) *FallbackRecoveryStrategy { return &FallbackRecoveryStrategy{ fallbackFn: fallbackFn, logger: logger, } } // CanRecover retourne toujours true pour le fallback func (f *FallbackRecoveryStrategy) CanRecover(err error) bool { return true } // Recover exécute la fonction de fallback func (f *FallbackRecoveryStrategy) Recover(ctx context.Context, err error) error { if f.fallbackFn == nil { return fmt.Errorf("no fallback function provided") } if f.logger != nil { f.logger.Info("Using fallback recovery strategy", zap.Error(err)) } return f.fallbackFn() } // CircuitBreakerRecoveryStrategy récupère d'une erreur en vérifiant l'état du circuit breaker type CircuitBreakerRecoveryStrategy struct { checkFn func() bool logger *zap.Logger } // NewCircuitBreakerRecoveryStrategy crée une nouvelle stratégie basée sur le circuit breaker func NewCircuitBreakerRecoveryStrategy(checkFn func() bool, logger *zap.Logger) *CircuitBreakerRecoveryStrategy { return &CircuitBreakerRecoveryStrategy{ checkFn: checkFn, logger: logger, } } // CanRecover vérifie si le circuit breaker est ouvert (on peut récupérer si ouvert) func (c *CircuitBreakerRecoveryStrategy) CanRecover(err error) bool { if c.checkFn == nil { return false } // Si le circuit breaker est ouvert (checkFn retourne true), on peut récupérer en attendant // Si fermé (checkFn retourne false), pas besoin de récupération return c.checkFn() } // Recover attend que le circuit breaker se ferme func (c *CircuitBreakerRecoveryStrategy) Recover(ctx context.Context, err error) error { if c.logger != nil { c.logger.Warn("Circuit breaker is open, waiting for recovery", zap.Error(err)) } // Utiliser le contexte existant (qui peut déjà avoir un timeout) // Ne pas créer un nouveau timeout si le contexte en a déjà un if _, hasTimeout := ctx.Deadline(); !hasTimeout { timeout := 30 * time.Second var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) defer cancel() } ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return fmt.Errorf("timeout waiting for circuit breaker to close: %w", ctx.Err()) case <-ticker.C: if c.checkFn() { // Circuit breaker fermé, on peut réessayer return nil } } } } // CompositeRecoveryStrategy combine plusieurs stratégies de récupération type CompositeRecoveryStrategy struct { strategies []ErrorRecoveryStrategy logger *zap.Logger } // NewCompositeRecoveryStrategy crée une stratégie composite func NewCompositeRecoveryStrategy(strategies []ErrorRecoveryStrategy, logger *zap.Logger) *CompositeRecoveryStrategy { return &CompositeRecoveryStrategy{ strategies: strategies, logger: logger, } } // CanRecover vérifie si au moins une stratégie peut récupérer func (c *CompositeRecoveryStrategy) CanRecover(err error) bool { for _, strategy := range c.strategies { if strategy.CanRecover(err) { return true } } return false } // Recover tente de récupérer en utilisant la première stratégie applicable func (c *CompositeRecoveryStrategy) Recover(ctx context.Context, err error) error { for _, strategy := range c.strategies { if strategy.CanRecover(err) { if c.logger != nil { c.logger.Debug("Attempting recovery with strategy", zap.String("type", fmt.Sprintf("%T", strategy))) } recoveryErr := strategy.Recover(ctx, err) if recoveryErr == nil { return nil } // Si cette stratégie échoue, essayer la suivante if c.logger != nil { c.logger.Warn("Recovery strategy failed, trying next", zap.Error(recoveryErr)) } } } return fmt.Errorf("all recovery strategies failed: %w", err) } // RecoverWithStrategies tente de récupérer d'une erreur en utilisant plusieurs stratégies func RecoverWithStrategies(ctx context.Context, err error, strategies []ErrorRecoveryStrategy, logger *zap.Logger) error { if len(strategies) == 0 { return fmt.Errorf("no recovery strategies provided: %w", err) } composite := NewCompositeRecoveryStrategy(strategies, logger) return composite.Recover(ctx, err) } // IsTemporaryError vérifie si une erreur est temporaire et peut être retryée func IsTemporaryError(err error) bool { if err == nil { return false } // Vérifier les erreurs temporaires connues var tempErr interface { Temporary() bool } if errors.As(err, &tempErr) { return tempErr.Temporary() } // Vérifier les patterns d'erreurs temporaires return IsRetryableError(err) } // IsPermanentError vérifie si une erreur est permanente et ne doit pas être retryée func IsPermanentError(err error) bool { return !IsTemporaryError(err) }