package services import ( "context" "fmt" "net/http" "time" "veza-backend-api/internal/metrics" "github.com/sony/gobreaker" "go.uber.org/zap" ) // CircuitBreakerHTTPClient wraps an HTTP client with circuit breaker protection // MOD-P2-007: Circuit breaker pour protéger contre dépendances lentes/indisponibles type CircuitBreakerHTTPClient struct { client *http.Client circuitBreaker *gobreaker.CircuitBreaker logger *zap.Logger } // NewCircuitBreakerHTTPClient creates a new HTTP client with circuit breaker // MOD-P2-007: Circuit breaker avec seuils configurables func NewCircuitBreakerHTTPClient(client *http.Client, name string, logger *zap.Logger) *CircuitBreakerHTTPClient { if client == nil { client = &http.Client{Timeout: 10 * time.Second} } if logger == nil { logger = zap.NewNop() } // Configuration circuit breaker: // - MaxRequests: 3 requêtes simultanées max // - Interval: 60s pour réinitialiser les compteurs // - Timeout: 30s avant de passer en half-open // - ReadyToTrip: s'ouvre après 5 échecs consécutifs cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{ Name: name, MaxRequests: 3, Interval: 60 * time.Second, Timeout: 30 * time.Second, ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.ConsecutiveFailures >= 5 }, OnStateChange: func(cbName string, from gobreaker.State, to gobreaker.State) { logger.Info("Circuit breaker state changed", zap.String("name", cbName), zap.String("from", from.String()), zap.String("to", to.String())) // MOD-P2-007: Mettre à jour les métriques lors du changement d'état // Note: On ne peut pas accéder à cb ici car il n'est pas encore créé // Les métriques seront mises à jour dans Do() après chaque requête }, }) return &CircuitBreakerHTTPClient{ client: client, circuitBreaker: cb, logger: logger, } } // Do executes an HTTP request with circuit breaker protection // MOD-P2-007: Wrapper pour http.Client.Do avec circuit breaker func (c *CircuitBreakerHTTPClient) Do(req *http.Request) (*http.Response, error) { // MOD-P2-007: Mettre à jour les métriques avant l'exécution counts := c.circuitBreaker.Counts() state := c.circuitBreaker.State() metrics.UpdateCircuitBreakerMetrics(c.circuitBreaker.Name(), counts, state) // Exécuter la requête via circuit breaker result, err := c.circuitBreaker.Execute(func() (interface{}, error) { resp, err := c.client.Do(req) if err != nil { // MOD-P2-007: Enregistrer l'échec dans les métriques metrics.RecordCircuitBreakerRequest(c.circuitBreaker.Name(), "failure") return nil, err } // Considérer les codes 5xx comme des erreurs pour le circuit breaker if resp.StatusCode >= 500 { resp.Body.Close() // MOD-P2-007: Enregistrer l'échec dans les métriques metrics.RecordCircuitBreakerRequest(c.circuitBreaker.Name(), "failure") return nil, fmt.Errorf("server error: %d", resp.StatusCode) } // MOD-P2-007: Enregistrer le succès dans les métriques metrics.RecordCircuitBreakerRequest(c.circuitBreaker.Name(), "success") return resp, nil }) if err != nil { // Circuit breaker ouvert ou erreur HTTP if err == gobreaker.ErrOpenState { // MOD-P2-007: Enregistrer le rejet dans les métriques metrics.RecordCircuitBreakerRequest(c.circuitBreaker.Name(), "rejected") c.logger.Warn("Circuit breaker is open, request rejected", zap.String("circuit_breaker", c.circuitBreaker.Name()), zap.String("url", req.URL.String())) return nil, fmt.Errorf("circuit breaker is open: service unavailable") } return nil, err } // Type assertion pour récupérer la réponse if httpResp, ok := result.(*http.Response); ok { // MOD-P2-007: Mettre à jour les métriques après succès counts = c.circuitBreaker.Counts() state = c.circuitBreaker.State() metrics.UpdateCircuitBreakerMetrics(c.circuitBreaker.Name(), counts, state) return httpResp, nil } return nil, fmt.Errorf("unexpected response type from circuit breaker") } // DoWithContext executes an HTTP request with context and circuit breaker protection // MOD-P2-007: Version avec contexte pour timeout/cancellation func (c *CircuitBreakerHTTPClient) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) { // Créer une nouvelle requête avec le contexte req = req.WithContext(ctx) return c.Do(req) }