Per-host circuit breaker с retry для CDN fetch и RIPEstat; порог 5 ошибок, cooldown 30s. Co-authored-by: Cursor <cursoragent@cursor.com>
56 lines
998 B
Go
56 lines
998 B
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
|
|
}
|
|
}
|
|
|
|
// ResetHostBreakers clears all circuit breakers (tests only).
|
|
func ResetHostBreakers() {
|
|
hostBreakers = sync.Map{}
|
|
}
|