Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

108 lines
2.4 KiB
Go

package daemon
import (
"context"
"sync"
"time"
)
// circuitBreaker skips a remote that has failed K times in a row until a
// cooldown elapses, so a chronically-dead remote stops costing a per-call
// timeout. Half-opens after the cooldown (one trial call) by resetting.
type circuitBreaker struct {
mu sync.Mutex
threshold int
cooldown time.Duration
state map[string]*breakerState
}
type breakerState struct {
failures int
openUntil time.Time
}
func newCircuitBreaker(threshold int, cooldown time.Duration) *circuitBreaker {
return &circuitBreaker{
threshold: threshold,
cooldown: cooldown,
state: map[string]*breakerState{},
}
}
func (b *circuitBreaker) isOpen(slug string) bool {
b.mu.Lock()
defer b.mu.Unlock()
st := b.state[slug]
if st == nil || st.openUntil.IsZero() {
return false
}
if time.Now().After(st.openUntil) {
// Cooldown elapsed — half-open: let the next call through.
st.openUntil = time.Time{}
st.failures = 0
return false
}
return true
}
func (b *circuitBreaker) fail(slug string) {
b.mu.Lock()
defer b.mu.Unlock()
st := b.state[slug]
if st == nil {
st = &breakerState{}
b.state[slug] = st
}
st.failures++
if st.failures >= b.threshold {
st.openUntil = time.Now().Add(b.cooldown)
}
}
func (b *circuitBreaker) success(slug string) {
b.mu.Lock()
defer b.mu.Unlock()
if st := b.state[slug]; st != nil {
st.failures = 0
st.openUntil = time.Time{}
}
}
// healthCache memoises each remote's /v1/health advertisement for a short
// TTL so the per-call capability + readiness negotiation costs at most one
// extra round-trip per remote per window.
type healthCache struct {
mu sync.Mutex
ttl time.Duration
entries map[string]healthEntry
}
type healthEntry struct {
h RemoteHealth
at time.Time
err error
}
func newHealthCache(ttl time.Duration) *healthCache {
return &healthCache{ttl: ttl, entries: map[string]healthEntry{}}
}
func (c *healthCache) get(ctx context.Context, cli *ServerClient, timeout time.Duration) (RemoteHealth, error) {
slug := cli.Entry.Slug
c.mu.Lock()
if e, ok := c.entries[slug]; ok && time.Since(e.at) < c.ttl {
c.mu.Unlock()
return e.h, e.err
}
c.mu.Unlock()
hctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
h, err := cli.FetchHealth(hctx)
c.mu.Lock()
c.entries[slug] = healthEntry{h: h, at: time.Now(), err: err}
c.mu.Unlock()
return h, err
}