91 lines
breaker/breaker.go
Circuit breaker with Closed, Open, and HalfOpen state transitions.
// Package breaker implements a circuit breaker for protecting upstream service calls.
package breaker
 
import (
	"sync"
	"time"
)
 
// State represents the current circuit state.
type State int
 
const (
	// Closed is the normal operating state; calls are allowed through.
	Closed State = iota
	// Open means the circuit has tripped; calls are rejected immediately.
	Open
	// HalfOpen means one probe call is permitted to test recovery.
	HalfOpen
)
 
// Breaker tracks consecutive failures and opens the circuit when the threshold is reached.
// After halfOpenDelay the circuit allows one probe; success closes it, failure reopens it.
type Breaker struct {
	mu            sync.Mutex
	state         State
	failures      int
	threshold     int
	openedAt      time.Time
	halfOpenDelay time.Duration
}
 
// New returns a Breaker that opens after threshold consecutive failures
// and waits halfOpenDelay before allowing a probe.
func New(threshold int, halfOpenDelay time.Duration) *Breaker {
	return &Breaker{threshold: threshold, halfOpenDelay: halfOpenDelay}
}
 
// Allow reports whether a call is permitted and updates the circuit state.
// In Closed state all calls are permitted.
// In Open state calls are rejected; once halfOpenDelay has elapsed, the circuit
// transitions to HalfOpen and permits exactly one probe.
// In HalfOpen state all calls are rejected until RecordSuccess or RecordFailure is called.
func (b *Breaker) Allow() bool {
	b.mu.Lock()
	defer b.mu.Unlock()
	switch b.state {
	case Closed:
		return true
	case Open:
		if time.Since(b.openedAt) >= b.halfOpenDelay {
			b.state = HalfOpen
			return true
		}
		return false
	case HalfOpen:
		return false
	}
	return false
}
 
// RecordSuccess records a successful call result.
// If the circuit is HalfOpen the probe succeeded: transition to Closed
// and reset the consecutive failure counter to zero.
func (b *Breaker) RecordSuccess() {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.state == HalfOpen {
		b.state = Closed
	}
}
 
// RecordFailure records a failed call result.
// In Closed state, increments the failure counter and opens the circuit
// when the counter reaches threshold.
// In HalfOpen state, the probe failed: reopen the circuit and start a new cooldown.
func (b *Breaker) RecordFailure() {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.failures++
	if b.state == Closed && b.failures >= b.threshold {
		b.state = Open
		b.openedAt = time.Now()
	}
}
 
// State returns the current circuit state.
func (b *Breaker) CurrentState() State {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.state
}