// Package circuitbreaker implements a per-provider circuit breaker. // States: Closed (normal) → Open (failing, rejects requests) → HalfOpen (testing recovery). // Transition Closed→Open: after `threshold` consecutive failures. // Transition Open→HalfOpen: after `openTTL` has elapsed. // Transition HalfOpen→Closed: on the first successful request. // Transition HalfOpen→Open: on failure during half-open test. package circuitbreaker import ( "sync" "time" ) // State represents the circuit breaker state for a provider. type State int const ( Closed State = iota // Normal — requests allowed Open // Tripped — requests rejected HalfOpen // Recovery probe — one request allowed ) func (s State) String() string { switch s { case Closed: return "closed" case Open: return "open" case HalfOpen: return "half_open" default: return "unknown" } } // Status is the read-only snapshot returned by the API. type Status struct { Provider string `json:"provider"` State string `json:"state"` Failures int `json:"failures"` OpenedAt string `json:"opened_at,omitempty"` // RFC3339, only when Open/HalfOpen } type entry struct { state State failures int openedAt time.Time // halfOpenInFlight prevents concurrent requests during HalfOpen probe. halfOpenInFlight bool } // Breaker is a thread-safe circuit breaker for multiple providers. type Breaker struct { mu sync.Mutex states map[string]*entry threshold int openTTL time.Duration } // New creates a Breaker. // - threshold: consecutive failures before opening the circuit. // - openTTL: how long to wait in Open state before transitioning to HalfOpen. func New(threshold int, openTTL time.Duration) *Breaker { return &Breaker{ states: make(map[string]*entry), threshold: threshold, openTTL: openTTL, } } func (b *Breaker) get(provider string) *entry { e, ok := b.states[provider] if !ok { e = &entry{state: Closed} b.states[provider] = e } return e } // Allow returns true if a request to the given provider should proceed. // It also handles the Open→HalfOpen transition when the TTL has expired. func (b *Breaker) Allow(provider string) bool { b.mu.Lock() defer b.mu.Unlock() e := b.get(provider) switch e.state { case Closed: return true case Open: if time.Since(e.openedAt) >= b.openTTL { // Transition to HalfOpen — allow exactly one probe. if !e.halfOpenInFlight { e.state = HalfOpen e.halfOpenInFlight = true return true } } return false case HalfOpen: // Only one in-flight request allowed during HalfOpen. if !e.halfOpenInFlight { e.halfOpenInFlight = true return true } return false } return true } // Success records a successful response from a provider. // Any non-Open circuit resets the failure counter; HalfOpen transitions to Closed. func (b *Breaker) Success(provider string) { b.mu.Lock() defer b.mu.Unlock() e := b.get(provider) e.failures = 0 e.state = Closed e.halfOpenInFlight = false } // Failure records a failed response from a provider. // If threshold is reached the circuit transitions to Open. // A failure during HalfOpen re-opens the circuit immediately. func (b *Breaker) Failure(provider string) { b.mu.Lock() defer b.mu.Unlock() e := b.get(provider) e.halfOpenInFlight = false switch e.state { case Closed: e.failures++ if e.failures >= b.threshold { e.state = Open e.openedAt = time.Now() } case HalfOpen: // Re-open immediately. e.state = Open e.openedAt = time.Now() e.failures++ } } // Status returns a read-only snapshot of the circuit state for a provider. func (b *Breaker) Status(provider string) Status { b.mu.Lock() defer b.mu.Unlock() e := b.get(provider) s := Status{ Provider: provider, State: e.state.String(), Failures: e.failures, } if e.state == Open || e.state == HalfOpen { s.OpenedAt = e.openedAt.Format(time.RFC3339) } return s } // Statuses returns snapshots for all known providers. func (b *Breaker) Statuses() []Status { b.mu.Lock() defer b.mu.Unlock() out := make([]Status, 0, len(b.states)) for name, e := range b.states { s := Status{ Provider: name, State: e.state.String(), Failures: e.failures, } if e.state == Open || e.state == HalfOpen { s.OpenedAt = e.openedAt.Format(time.RFC3339) } out = append(out, s) } return out }