e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
package mcp
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// CircuitBreakerConfig configures circuit breaking for the MCP gateway.
|
|
// When a downstream service fails repeatedly, the circuit opens and
|
|
// subsequent calls are rejected immediately until the service recovers.
|
|
type CircuitBreakerConfig struct {
|
|
// MaxFailures is the number of consecutive failures before the circuit opens.
|
|
// Default: 5
|
|
MaxFailures int
|
|
|
|
// Timeout is how long the circuit stays open before allowing a probe request.
|
|
// Default: 30s
|
|
Timeout time.Duration
|
|
|
|
// MaxHalfOpen is the number of probe requests allowed in the half-open state.
|
|
// If they all succeed, the circuit closes. If any fail, it re-opens.
|
|
// Default: 1
|
|
MaxHalfOpen int
|
|
}
|
|
|
|
// circuitState represents the state of a circuit breaker.
|
|
type circuitState int
|
|
|
|
const (
|
|
circuitClosed circuitState = iota // healthy, requests flow through
|
|
circuitOpen // tripped, requests are rejected
|
|
circuitHalfOpen // testing recovery with limited requests
|
|
)
|
|
|
|
func (s circuitState) String() string {
|
|
switch s {
|
|
case circuitClosed:
|
|
return "closed"
|
|
case circuitOpen:
|
|
return "open"
|
|
case circuitHalfOpen:
|
|
return "half-open"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// circuitBreaker tracks failure state for a single tool/service endpoint.
|
|
type circuitBreaker struct {
|
|
mu sync.Mutex
|
|
state circuitState
|
|
failures int
|
|
maxFailures int
|
|
timeout time.Duration
|
|
maxHalfOpen int
|
|
halfOpenUsed int
|
|
lastFailure time.Time
|
|
}
|
|
|
|
func newCircuitBreaker(cfg CircuitBreakerConfig) *circuitBreaker {
|
|
maxFailures := cfg.MaxFailures
|
|
if maxFailures <= 0 {
|
|
maxFailures = 5
|
|
}
|
|
timeout := cfg.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
maxHalfOpen := cfg.MaxHalfOpen
|
|
if maxHalfOpen <= 0 {
|
|
maxHalfOpen = 1
|
|
}
|
|
return &circuitBreaker{
|
|
state: circuitClosed,
|
|
maxFailures: maxFailures,
|
|
timeout: timeout,
|
|
maxHalfOpen: maxHalfOpen,
|
|
}
|
|
}
|
|
|
|
// Allow checks whether a request should be allowed through.
|
|
// Returns nil if allowed, error if the circuit is open.
|
|
func (cb *circuitBreaker) Allow() error {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
switch cb.state {
|
|
case circuitClosed:
|
|
return nil
|
|
case circuitOpen:
|
|
if time.Since(cb.lastFailure) > cb.timeout {
|
|
cb.state = circuitHalfOpen
|
|
cb.halfOpenUsed = 0
|
|
return nil
|
|
}
|
|
return fmt.Errorf("circuit breaker open (consecutive failures: %d)", cb.failures)
|
|
case circuitHalfOpen:
|
|
if cb.halfOpenUsed < cb.maxHalfOpen {
|
|
cb.halfOpenUsed++
|
|
return nil
|
|
}
|
|
return fmt.Errorf("circuit breaker half-open (probe limit reached)")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RecordSuccess records a successful call. If half-open, closes the circuit.
|
|
func (cb *circuitBreaker) RecordSuccess() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
cb.failures = 0
|
|
cb.state = circuitClosed
|
|
}
|
|
|
|
// RecordFailure records a failed call. May trip the circuit open.
|
|
func (cb *circuitBreaker) RecordFailure() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
cb.failures++
|
|
cb.lastFailure = time.Now()
|
|
|
|
switch cb.state {
|
|
case circuitClosed:
|
|
if cb.failures >= cb.maxFailures {
|
|
cb.state = circuitOpen
|
|
}
|
|
case circuitHalfOpen:
|
|
// Probe failed, re-open
|
|
cb.state = circuitOpen
|
|
}
|
|
}
|
|
|
|
// State returns the current circuit state.
|
|
func (cb *circuitBreaker) State() circuitState {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
// Check for automatic transition from open -> half-open
|
|
if cb.state == circuitOpen && time.Since(cb.lastFailure) > cb.timeout {
|
|
cb.state = circuitHalfOpen
|
|
cb.halfOpenUsed = 0
|
|
}
|
|
return cb.state
|
|
}
|