Add typed jevdecide HTTP client (circuit, metrics) and coordinator DecideAssist wiring behind JEV_DECIDE_MODE (default off). Offline eval and synthetic soak evidence gate realtime/profile before assist rollout.
91 lines
1.8 KiB
Go
91 lines
1.8 KiB
Go
package jevdecide
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultFailThreshold = 5
|
|
defaultOpenFor = 60 * time.Second
|
|
)
|
|
|
|
// CircuitBreaker opens after consecutive failures and half-opens after a cool-down.
|
|
type CircuitBreaker struct {
|
|
mu sync.Mutex
|
|
failures int
|
|
failThreshold int
|
|
openUntil time.Time
|
|
openFor time.Duration
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewCircuitBreaker builds a breaker with the given thresholds.
|
|
func NewCircuitBreaker(failThreshold int, openFor time.Duration) *CircuitBreaker {
|
|
if failThreshold <= 0 {
|
|
failThreshold = defaultFailThreshold
|
|
}
|
|
if openFor <= 0 {
|
|
openFor = defaultOpenFor
|
|
}
|
|
return &CircuitBreaker{
|
|
failThreshold: failThreshold,
|
|
openFor: openFor,
|
|
now: time.Now,
|
|
}
|
|
}
|
|
|
|
// Allow reports whether a call may proceed.
|
|
func (c *CircuitBreaker) Allow() bool {
|
|
if c == nil {
|
|
return true
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.openUntil.IsZero() {
|
|
return true
|
|
}
|
|
if c.now().After(c.openUntil) {
|
|
// half-open: allow one probe
|
|
c.openUntil = time.Time{}
|
|
c.failures = c.failThreshold - 1
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RecordSuccess resets the breaker.
|
|
func (c *CircuitBreaker) RecordSuccess() {
|
|
if c == nil {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.failures = 0
|
|
c.openUntil = time.Time{}
|
|
}
|
|
|
|
// RecordFailure increments consecutive failures and may open the circuit.
|
|
func (c *CircuitBreaker) RecordFailure() {
|
|
if c == nil {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.failures++
|
|
if c.failures >= c.failThreshold {
|
|
c.openUntil = c.now().Add(c.openFor)
|
|
c.failures = 0
|
|
}
|
|
}
|
|
|
|
// Open reports whether the circuit is currently open (for tests/metrics).
|
|
func (c *CircuitBreaker) Open() bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return !c.openUntil.IsZero() && c.now().Before(c.openUntil)
|
|
}
|