JSON slog в ключевых пакетах; Prometheus path_group, job_audit_depth, upstream breaker; job_audit ClaimQueued/ReclaimStaleRunning + Adopt loop для HA после рестарта. Co-authored-by: Cursor <cursoragent@cursor.com>
74 lines
1.4 KiB
Go
74 lines
1.4 KiB
Go
package httpclient
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultBreakerThreshold = 5
|
|
defaultBreakerCooldown = 30 * time.Second
|
|
)
|
|
|
|
type hostBreaker struct {
|
|
mu sync.Mutex
|
|
failures int
|
|
openUntil time.Time
|
|
}
|
|
|
|
var hostBreakers sync.Map // string -> *hostBreaker
|
|
|
|
func breakerForHost(host string) *hostBreaker {
|
|
if host == "" {
|
|
host = "_"
|
|
}
|
|
v, _ := hostBreakers.LoadOrStore(host, &hostBreaker{})
|
|
return v.(*hostBreaker)
|
|
}
|
|
|
|
func (b *hostBreaker) allow() bool {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return time.Now().After(b.openUntil)
|
|
}
|
|
|
|
func (b *hostBreaker) recordSuccess() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.failures = 0
|
|
b.openUntil = time.Time{}
|
|
}
|
|
|
|
func (b *hostBreaker) recordFailure() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.failures++
|
|
if b.failures >= defaultBreakerThreshold {
|
|
b.openUntil = time.Now().Add(defaultBreakerCooldown)
|
|
b.failures = 0
|
|
}
|
|
}
|
|
|
|
// SnapshotBreakers returns whether each known host breaker is currently open.
|
|
func SnapshotBreakers() map[string]bool {
|
|
out := map[string]bool{}
|
|
hostBreakers.Range(func(key, value any) bool {
|
|
host, _ := key.(string)
|
|
b, _ := value.(*hostBreaker)
|
|
if b == nil {
|
|
return true
|
|
}
|
|
b.mu.Lock()
|
|
open := time.Now().Before(b.openUntil)
|
|
b.mu.Unlock()
|
|
out[host] = open
|
|
return true
|
|
})
|
|
return out
|
|
}
|
|
|
|
// ResetHostBreakers clears all circuit breakers (tests only).
|
|
func ResetHostBreakers() {
|
|
hostBreakers = sync.Map{}
|
|
}
|