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{} }