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