- Created DataExportService for comprehensive user data export (GDPR compliance) - Exports all user data: profile, settings, tracks, playlists, comments, likes, analytics, federated identities, roles - Added ExportUserData method to retrieve all user data from database - Added ExportUserDataAsJSON method to export as downloadable JSON file - Added endpoint GET /api/v1/users/me/export that returns JSON file download - Comprehensive unit tests for export service - Proper error handling and logging Phase: PHASE-6 Priority: P2 Progress: 118/267 (44.19%)
215 lines
6.4 KiB
Go
215 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)
|
|
}
|
|
|