76 lines
1.7 KiB
Go
76 lines
1.7 KiB
Go
package main
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// CircuitState represents the breaker's current state.
|
|
type CircuitState int
|
|
|
|
const (
|
|
StateClosed CircuitState = iota // normal — requests pass through
|
|
StateOpen // tripped — fail-open
|
|
StateHalfOpen // testing recovery
|
|
)
|
|
|
|
// CircuitBreaker implements a simple fail-open pattern.
|
|
// When failures exceed the threshold within the window, the breaker opens.
|
|
// After a cooldown, it moves to half-open to test recovery.
|
|
type CircuitBreaker struct {
|
|
mu sync.Mutex
|
|
state CircuitState
|
|
failures int
|
|
lastFailure time.Time
|
|
maxFailures int
|
|
window time.Duration
|
|
cooldown time.Duration
|
|
openedAt time.Time
|
|
}
|
|
|
|
func newCircuitBreaker() *CircuitBreaker {
|
|
return &CircuitBreaker{
|
|
state: StateClosed,
|
|
maxFailures: 5,
|
|
window: 30 * time.Second,
|
|
cooldown: 10 * time.Second,
|
|
}
|
|
}
|
|
|
|
// state returns the current state, testing for recovery if half-open and cooldown elapsed.
|
|
func (cb *CircuitBreaker) State() CircuitState {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
if cb.state == StateOpen && time.Since(cb.openedAt) > cb.cooldown {
|
|
cb.state = StateHalfOpen
|
|
}
|
|
// Reset failure count if window elapsed.
|
|
if time.Since(cb.lastFailure) > cb.window {
|
|
cb.failures = 0
|
|
if cb.state == StateHalfOpen {
|
|
cb.state = StateClosed
|
|
}
|
|
}
|
|
return cb.state
|
|
}
|
|
|
|
func (cb *CircuitBreaker) RecordSuccess() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
if cb.state == StateHalfOpen {
|
|
cb.state = StateClosed
|
|
}
|
|
cb.failures = 0
|
|
}
|
|
|
|
func (cb *CircuitBreaker) RecordFailure() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
cb.failures++
|
|
cb.lastFailure = time.Now()
|
|
if cb.failures >= cb.maxFailures {
|
|
cb.state = StateOpen
|
|
cb.openedAt = time.Now()
|
|
}
|
|
}
|