veza/veza-backend-api/internal/recovery/error_recovery.go

214 lines
6.4 KiB
Go

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