package app import ( "os" "strings" ) func newDNSRunCooldown() *dnsRunCooldown { enabled := true switch strings.ToLower(strings.TrimSpace(os.Getenv("RESOLVE_DNS_COOLDOWN_ENABLED"))) { case "0", "false", "no", "off": enabled = false } c := &dnsRunCooldown{ enabled: enabled, minAttempts: envInt("RESOLVE_DNS_COOLDOWN_MIN_ATTEMPTS", 300), timeoutRatePct: envInt("RESOLVE_DNS_COOLDOWN_TIMEOUT_RATE_PCT", 70), failStreak: envInt("RESOLVE_DNS_COOLDOWN_FAIL_STREAK", 25), banSec: envInt("RESOLVE_DNS_COOLDOWN_BAN_SEC", 60), maxBanSec: envInt("RESOLVE_DNS_COOLDOWN_MAX_BAN_SEC", 300), temporaryAsError: true, byUpstream: map[string]*dnsCooldownState{}, } if c.minAttempts < 50 { c.minAttempts = 50 } if c.minAttempts > 2000 { c.minAttempts = 2000 } if c.timeoutRatePct < 40 { c.timeoutRatePct = 40 } if c.timeoutRatePct > 95 { c.timeoutRatePct = 95 } if c.failStreak < 8 { c.failStreak = 8 } if c.failStreak > 200 { c.failStreak = 200 } if c.banSec < 10 { c.banSec = 10 } if c.banSec > 3600 { c.banSec = 3600 } if c.maxBanSec < c.banSec { c.maxBanSec = c.banSec } if c.maxBanSec > 3600 { c.maxBanSec = 3600 } return c } func (c *dnsRunCooldown) configSnapshot() (enabled bool, minAttempts, timeoutRatePct, failStreak, banSec, maxBanSec int) { if c == nil { return false, 0, 0, 0, 0, 0 } return c.enabled, c.minAttempts, c.timeoutRatePct, c.failStreak, c.banSec, c.maxBanSec } func (c *dnsRunCooldown) stateFor(upstream string) *dnsCooldownState { if c.byUpstream == nil { c.byUpstream = map[string]*dnsCooldownState{} } st, ok := c.byUpstream[upstream] if ok { return st } st = &dnsCooldownState{} c.byUpstream[upstream] = st return st } func (c *dnsRunCooldown) shouldSkip(upstream string, now int64) bool { if c == nil || !c.enabled { return false } c.mu.Lock() defer c.mu.Unlock() st := c.stateFor(upstream) return st.BanUntil > now } func (c *dnsRunCooldown) observeSuccess(upstream string) { if c == nil || !c.enabled { return } c.mu.Lock() defer c.mu.Unlock() st := c.stateFor(upstream) st.Attempts++ st.FailStreak = 0 } func (c *dnsRunCooldown) observeError(upstream string, kind dnsErrorKind, now int64) (bool, int) { if c == nil || !c.enabled { return false, 0 } c.mu.Lock() defer c.mu.Unlock() st := c.stateFor(upstream) st.Attempts++ timeoutLike := kind == dnsErrorTimeout || (c.temporaryAsError && kind == dnsErrorTemporary) if timeoutLike { st.TimeoutLike++ st.FailStreak++ } else { st.FailStreak = 0 return false, 0 } if st.BanUntil > now { return false, 0 } rateBan := st.Attempts >= c.minAttempts && (st.TimeoutLike*100 >= c.timeoutRatePct*st.Attempts) streakBan := st.FailStreak >= c.failStreak if !rateBan && !streakBan { return false, 0 } st.BanLevel++ dur := c.banSec if st.BanLevel > 1 { for i := 1; i < st.BanLevel; i++ { dur *= 2 if dur >= c.maxBanSec { dur = c.maxBanSec break } } } if dur > c.maxBanSec { dur = c.maxBanSec } st.BanUntil = now + int64(dur) st.FailStreak = 0 return true, dur }