package resolver type ResolvePlanningInput struct { Domains []string Now int TTL int PrecheckDue bool PrecheckMaxDomains int StaleKeepSec int NegTTLNX int NegTTLTimeout int NegTTLTemporary int NegTTLOther int } type ResolvePlanningResult struct { Fresh map[string][]string ToResolve []string CacheNegativeHits int QuarantineHits int StaleHits int PrecheckScheduled int } func BuildResolvePlanning( in ResolvePlanningInput, domainCache *DomainCacheState, cacheSourceForHost func(string) DomainCacheSource, logf func(string, ...any), ) ResolvePlanningResult { result := ResolvePlanningResult{ Fresh: map[string][]string{}, } if domainCache == nil { result.ToResolve = append(result.ToResolve, in.Domains...) return result } resolveSource := cacheSourceForHost if resolveSource == nil { resolveSource = func(string) DomainCacheSource { return DomainCacheSourceDirect } } for _, d := range in.Domains { source := resolveSource(d) if ips, ok := domainCache.Get(d, source, in.Now, in.TTL); ok { result.Fresh[d] = ips if logf != nil { logf("cache hit[%s]: %s -> %v", source, d, ips) } continue } // Quarantine has priority over negative TTL cache so 24h quarantine // is not silently overridden by shorter negative cache windows. if state, age, ok := domainCache.GetQuarantine(d, source, in.Now); ok { kind, hasKind := domainCache.GetLastErrorKind(d, source) timeoutKind := hasKind && kind == DNSErrorTimeout if in.PrecheckDue && result.PrecheckScheduled < in.PrecheckMaxDomains { // Timeout-based quarantine is rechecked in background batch and should // not flood trace with per-domain debug lines. if timeoutKind { result.QuarantineHits++ if in.StaleKeepSec > 0 { if staleIPs, staleAge, ok := domainCache.GetStale(d, source, in.Now, in.StaleKeepSec); ok { result.StaleHits++ result.Fresh[d] = staleIPs if logf != nil { logf("cache stale-keep (quarantine)[age=%ds]: %s -> %v", staleAge, d, staleIPs) } } } continue } result.PrecheckScheduled++ result.ToResolve = append(result.ToResolve, d) if logf != nil { logf("precheck schedule[quarantine/%s age=%ds]: %s (%s)", state, age, d, source) } continue } result.QuarantineHits++ if logf != nil { logf("cache quarantine hit[%s age=%ds]: %s (%s)", state, age, d, source) } if in.StaleKeepSec > 0 { if staleIPs, staleAge, ok := domainCache.GetStale(d, source, in.Now, in.StaleKeepSec); ok { result.StaleHits++ result.Fresh[d] = staleIPs if logf != nil { logf("cache stale-keep (quarantine)[age=%ds]: %s -> %v", staleAge, d, staleIPs) } } } continue } if kind, age, ok := domainCache.GetNegative(d, source, in.Now, in.NegTTLNX, in.NegTTLTimeout, in.NegTTLTemporary, in.NegTTLOther); ok { if in.PrecheckDue && result.PrecheckScheduled < in.PrecheckMaxDomains { if kind == DNSErrorTimeout { result.CacheNegativeHits++ continue } result.PrecheckScheduled++ result.ToResolve = append(result.ToResolve, d) if logf != nil { logf("precheck schedule[negative/%s age=%ds]: %s (%s)", kind, age, d, source) } continue } result.CacheNegativeHits++ if logf != nil { logf("cache neg hit[%s/%s age=%ds]: %s", source, kind, age, d) } continue } result.ToResolve = append(result.ToResolve, d) } return result }