package circuitbreaker import ( "errors" "sync" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/validator" ) // ErrOpen is returned by Allow when the breaker is rejecting calls. // // ErrOpen 是 Open 态 Allow() 的返回值. var ErrOpen = errors.New("circuitbreaker: circuit is open") // State enumerates the three breaker states. // // State 是三态熔断器的状态枚举. type State int // Breaker states. // // 熔断器状态. const ( StateClosed State = iota StateOpen StateHalfOpen ) // String returns a human-readable state name for logs / events. // // String 返回人类可读的状态名, 供日志 / 事件使用. func (s State) String() string { switch s { case StateClosed: return "closed" case StateOpen: return "open" case StateHalfOpen: return "half-open" default: return "unknown" } } // Clock abstracts time for tests. Production callers use the default // realClock; tests inject a fake to drive cooldown without sleeping. // // Clock 为测试抽象时间. 生产走默认 realClock; 测试注入 fake 让 cooldown // 推进不需要真 sleep. type Clock interface { Now() time.Time } type realClock struct{} func (realClock) Now() time.Time { return time.Now() } // Defaults used when Config fields are left zero. // // Config 字段为零时使用的默认值. const ( DefaultThreshold = 5 DefaultCooldown = 60 * time.Second ) // Config constructs a CircuitBreaker. Zero or negative values are // replaced by the Default* constants; a nil Clock uses a wall clock. // // Config 构造 CircuitBreaker. 零或负值用 Default* 常量替换; nil Clock // 用真 wall clock. type Config struct { // Threshold is the number of consecutive RecordFailure calls in // Closed state that trip the breaker to Open. // // Threshold 是 Closed 态连续 RecordFailure 跳 Open 所需的次数. Threshold int // Cooldown is how long the breaker stays Open before the next // Allow call flips it to HalfOpen. // // Cooldown 是 Open 态持续多久后下一次 Allow 转 HalfOpen. Cooldown time.Duration // Clock is the time source. nil uses a real wall clock. // // Clock 是时间源. nil 用真 wall clock. Clock Clock } // CircuitBreaker is a thread-safe three-state circuit breaker. // // CircuitBreaker 是线程安全的三态熔断器. type CircuitBreaker struct { threshold int cooldown time.Duration clock Clock mu sync.Mutex state State failCount int openedAt time.Time halfOpenProbe bool // true once HalfOpen Allow has been granted } // New constructs a CircuitBreaker. Zero / negative values in cfg are // replaced by defaults (DefaultThreshold / DefaultCooldown / realClock). // // New 构造 CircuitBreaker. cfg 中的零或负值用默认值替换 (DefaultThreshold // / DefaultCooldown / realClock). func New(cfg Config) *CircuitBreaker { threshold := cfg.Threshold if threshold <= 0 { threshold = DefaultThreshold } cooldown := cfg.Cooldown if cooldown <= 0 { cooldown = DefaultCooldown } clock := cfg.Clock if clock == nil { clock = realClock{} } return &CircuitBreaker{ threshold: threshold, cooldown: cooldown, clock: clock, state: StateClosed, } } // Allow returns nil if the caller may proceed, or ErrOpen if the // breaker is currently rejecting. Closed always allows; Open allows // only after the cooldown elapses (the breaker transitions to HalfOpen // on the granting Allow call); HalfOpen allows a single probe call // per cycle. // // Allow 返回 nil 表示可通过, 或 ErrOpen. Closed 放行; Open cooldown // 过后下一次 Allow 转 HalfOpen; HalfOpen 每周期只允许 1 次试探. func (b *CircuitBreaker) Allow() error { b.mu.Lock() defer b.mu.Unlock() switch b.state { case StateClosed: return nil case StateOpen: if b.clock.Now().Sub(b.openedAt) >= b.cooldown { b.state = StateHalfOpen b.halfOpenProbe = true return nil } return ErrOpen case StateHalfOpen: if b.halfOpenProbe { return ErrOpen } b.halfOpenProbe = true return nil default: return ErrOpen } } // RecordSuccess records a successful call. Resets the breaker to // Closed (clearing the failure counter) regardless of prior state. // // RecordSuccess 记录成功调用. 不管之前何态, 清零失败计数回 Closed. func (b *CircuitBreaker) RecordSuccess() { b.mu.Lock() defer b.mu.Unlock() b.state = StateClosed b.failCount = 0 b.halfOpenProbe = false } // RecordFailure records a failed call. In Closed state increments the // counter; on reaching Threshold trips to Open. In HalfOpen, any // failure immediately trips back to Open with the cooldown restarted // (prevents short-cycle flapping). Failures in Open state are a // no-op (already tripped). // // RecordFailure 记录失败调用. Closed 态累计计数, 达 Threshold 跳 Open. // HalfOpen 态任一失败立即回 Open 并重置 cooldown (防短周期 flapping). // Open 态记录 no-op. func (b *CircuitBreaker) RecordFailure() { b.mu.Lock() defer b.mu.Unlock() switch b.state { case StateClosed: b.failCount++ if b.failCount >= b.threshold { b.state = StateOpen b.openedAt = b.clock.Now() } case StateHalfOpen: b.state = StateOpen b.openedAt = b.clock.Now() b.halfOpenProbe = false case StateOpen: // Already open; keep openedAt (cooldown measured from first trip). } } // State returns the current breaker state (for observation; use Allow // for gate decisions, not State). // // State 返回当前状态 (供观测; gate 决策用 Allow 不用 State). func (b *CircuitBreaker) State() State { b.mu.Lock() defer b.mu.Unlock() return b.state } // FailureCount returns the current failure counter (0 outside Closed). // // FailureCount 返回当前失败计数 (Closed 之外为 0). func (b *CircuitBreaker) FailureCount() int { b.mu.Lock() defer b.mu.Unlock() return b.failCount } // VerdictSink returns a function matching ValidatedTool's VerdictSink // signature (func(toolName string, v validator.Verdict)). It maps // Verdicts to RecordSuccess / RecordFailure according to Severity: // - SeverityBlock -> RecordFailure (the write was rejected) // - other (Warn, empty) -> RecordSuccess (write proceeded) // // The toolName argument is intentionally ignored by this baseline // implementation -- a single breaker protects all tools uniformly. // Platforms wanting per-tool breakers wrap multiple CircuitBreakers // and route in their own sink. // // VerdictSink 返回一个与 ValidatedTool VerdictSink 签名匹配的函数 // (func(toolName string, v validator.Verdict)). 按 Severity 把 Verdict // 映射到 RecordSuccess / RecordFailure: // - SeverityBlock -> RecordFailure (写被明确拒) // - 其他 (Warn / empty) -> RecordSuccess (写放行) // // toolName 本基础实现刻意忽略 -- 单个 breaker 统一保护所有工具. // 需要 per-tool breaker 的 platform 包装多个 CircuitBreaker 在自己 // 的 sink 中路由. func (b *CircuitBreaker) VerdictSink() func(toolName string, v validator.Verdict) { return func(_ string, v validator.Verdict) { if v.Severity == validator.SeverityBlock { b.RecordFailure() } else { b.RecordSuccess() } } }