Files
micro--go-micro/gateway/mcp/circuitbreaker.go
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

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
}