package resilience 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())) }, }) return &CircuitBreakerHTTPClient{ client: client, circuitBreaker: cb, logger: logger, } } // State returns the current circuit breaker state (for observability and testing). func (c *CircuitBreakerHTTPClient) State() gobreaker.State { return c.circuitBreaker.State() } // Counts returns the current circuit breaker counts (for observability and testing). func (c *CircuitBreakerHTTPClient) Counts() gobreaker.Counts { return c.circuitBreaker.Counts() } // Name returns the circuit breaker name. func (c *CircuitBreakerHTTPClient) Name() string { return c.circuitBreaker.Name() } // Do executes an HTTP request with circuit breaker protection func (c *CircuitBreakerHTTPClient) Do(req *http.Request) (*http.Response, error) { counts := c.circuitBreaker.Counts() state := c.circuitBreaker.State() metrics.UpdateCircuitBreakerMetrics(c.circuitBreaker.Name(), counts, state) result, err := c.circuitBreaker.Execute(func() (interface{}, error) { resp, err := c.client.Do(req) if err != nil { metrics.RecordCircuitBreakerRequest(c.circuitBreaker.Name(), "failure") return nil, err } if resp.StatusCode >= 500 { resp.Body.Close() metrics.RecordCircuitBreakerRequest(c.circuitBreaker.Name(), "failure") return nil, fmt.Errorf("server error: %d", resp.StatusCode) } metrics.RecordCircuitBreakerRequest(c.circuitBreaker.Name(), "success") return resp, nil }) if err != nil { if err == gobreaker.ErrOpenState { 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 } if httpResp, ok := result.(*http.Response); ok { 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 func (c *CircuitBreakerHTTPClient) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) { req = req.WithContext(ctx) return c.Do(req) }