veza/veza-backend-api/internal/services/circuit_breaker.go
2025-12-16 11:23:49 -05:00

123 lines
4.3 KiB
Go

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