veza/veza-backend-api/internal/resilience/circuit_breaker.go
senke b103a09a25 chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 16:43:21 +01:00

122 lines
3.9 KiB
Go

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