178 lines
4.5 KiB
Go
178 lines
4.5 KiB
Go
// Package httpclient provides circuit breaker protection for HTTP clients.
|
|
package httpclient
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ErrCircuitOpen is returned when the circuit breaker is open and requests are blocked.
|
|
var ErrCircuitOpen = errors.New("circuit breaker is open")
|
|
|
|
// CircuitState represents the current state of a circuit breaker.
|
|
type CircuitState int
|
|
|
|
const (
|
|
// CircuitClosed allows all requests through and monitors for failures.
|
|
CircuitClosed CircuitState = iota
|
|
// CircuitOpen blocks all requests and waits for the reset timeout.
|
|
CircuitOpen
|
|
// CircuitHalfOpen allows a single test request to determine if the circuit should close.
|
|
CircuitHalfOpen
|
|
)
|
|
|
|
func (s CircuitState) String() string {
|
|
switch s {
|
|
case CircuitClosed:
|
|
return "closed"
|
|
case CircuitOpen:
|
|
return "open"
|
|
case CircuitHalfOpen:
|
|
return "half-open"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// CircuitBreakerConfig configures the circuit breaker behavior.
|
|
type CircuitBreakerConfig struct {
|
|
// FailureThreshold is the number of consecutive failures before opening the circuit.
|
|
// Default: 5
|
|
FailureThreshold int
|
|
|
|
// ResetTimeout is how long to wait before attempting to close the circuit.
|
|
// Default: 30s
|
|
ResetTimeout time.Duration
|
|
|
|
// HalfOpenMaxRequests is the number of test requests to allow in half-open state.
|
|
// Default: 1
|
|
HalfOpenMaxRequests int
|
|
}
|
|
|
|
// DefaultCircuitBreakerConfig returns sensible defaults for circuit breaker configuration.
|
|
func DefaultCircuitBreakerConfig() CircuitBreakerConfig {
|
|
return CircuitBreakerConfig{
|
|
FailureThreshold: 5,
|
|
ResetTimeout: 30 * time.Second,
|
|
HalfOpenMaxRequests: 1,
|
|
}
|
|
}
|
|
|
|
// CircuitBreaker implements the circuit breaker pattern for protecting services
|
|
// from cascading failures. It tracks consecutive failures and opens the circuit
|
|
// when a threshold is reached, preventing further requests until a reset timeout.
|
|
type CircuitBreaker struct {
|
|
config CircuitBreakerConfig
|
|
|
|
mu sync.Mutex
|
|
state CircuitState
|
|
consecutiveFailures int
|
|
lastFailure time.Time
|
|
halfOpenRequests int
|
|
}
|
|
|
|
// NewCircuitBreaker creates a new circuit breaker with the given configuration.
|
|
// If config fields are zero, defaults are applied.
|
|
func NewCircuitBreaker(config CircuitBreakerConfig) *CircuitBreaker {
|
|
if config.FailureThreshold == 0 {
|
|
config.FailureThreshold = 5
|
|
}
|
|
if config.ResetTimeout == 0 {
|
|
config.ResetTimeout = 30 * time.Second
|
|
}
|
|
if config.HalfOpenMaxRequests == 0 {
|
|
config.HalfOpenMaxRequests = 1
|
|
}
|
|
|
|
return &CircuitBreaker{
|
|
config: config,
|
|
state: CircuitClosed,
|
|
}
|
|
}
|
|
|
|
// Allow checks if a request should be allowed through the circuit breaker.
|
|
// Returns nil if the request is allowed, ErrCircuitOpen if blocked.
|
|
func (cb *CircuitBreaker) Allow() error {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
switch cb.state {
|
|
case CircuitClosed:
|
|
return nil
|
|
|
|
case CircuitOpen:
|
|
// Check if reset timeout has elapsed
|
|
if time.Since(cb.lastFailure) >= cb.config.ResetTimeout {
|
|
cb.state = CircuitHalfOpen
|
|
cb.halfOpenRequests = 0
|
|
return nil
|
|
}
|
|
return ErrCircuitOpen
|
|
|
|
case CircuitHalfOpen:
|
|
// Allow limited requests in half-open state
|
|
if cb.halfOpenRequests < cb.config.HalfOpenMaxRequests {
|
|
cb.halfOpenRequests++
|
|
return nil
|
|
}
|
|
return ErrCircuitOpen
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RecordSuccess records a successful request and potentially closes an open circuit.
|
|
func (cb *CircuitBreaker) RecordSuccess() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
switch cb.state {
|
|
case CircuitHalfOpen:
|
|
// Test request succeeded, close the circuit
|
|
cb.state = CircuitClosed
|
|
cb.consecutiveFailures = 0
|
|
cb.halfOpenRequests = 0
|
|
|
|
case CircuitClosed:
|
|
// Reset failure count on success
|
|
cb.consecutiveFailures = 0
|
|
}
|
|
}
|
|
|
|
// RecordFailure records a failed request and potentially opens the circuit.
|
|
func (cb *CircuitBreaker) RecordFailure() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
cb.consecutiveFailures++
|
|
cb.lastFailure = time.Now()
|
|
|
|
switch cb.state {
|
|
case CircuitClosed:
|
|
// Check if we've hit the failure threshold
|
|
if cb.consecutiveFailures >= cb.config.FailureThreshold {
|
|
cb.state = CircuitOpen
|
|
}
|
|
|
|
case CircuitHalfOpen:
|
|
// Test request failed, re-open the circuit
|
|
cb.state = CircuitOpen
|
|
cb.halfOpenRequests = 0
|
|
}
|
|
}
|
|
|
|
// State returns the current state of the circuit breaker.
|
|
func (cb *CircuitBreaker) State() CircuitState {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
return cb.state
|
|
}
|
|
|
|
// ConsecutiveFailures returns the current consecutive failure count.
|
|
func (cb *CircuitBreaker) ConsecutiveFailures() int {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
return cb.consecutiveFailures
|
|
}
|