- 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
122 lines
3.9 KiB
Go
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)
|
|
}
|