123 lines
4.3 KiB
Go
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)
|
|
}
|