sp2-verify-1770324218/pkg/httpclient/circuit.go
jordan 160675237a
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-05 20:43:39 +00:00

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
}