mirror of
https://github.com/coredns/coredns.git
synced 2026-08-20 23:08:28 -04:00
plugin/cache: add prefer_positive stale policy (#8378)
* plugin/cache: add prefer_positive stale policy Add an opt-in serve_stale_policy that prefers an eligible success-cache answer over denial-cache entries while serve_stale is enabled. Preserve the existing ncache-first behavior when the policy is absent. Also classify SOA-backed CNAME NODATA responses in the cache so incomplete answers cannot be selected as positive stale responses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6 Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com> * plugin/cache: retain last-known-good positive answers Keep an answering success-cache item reachable when a later NOERROR or referral response overwrites the visible cache key without answering the question. This lets prefer_positive survive empty responses, referrals, and additional-only data while leaving policy-off lookup behavior unchanged. Return the exact accepted verify refresh item instead of re-reading an ambiguous cache key, avoiding expired TTL wraparound for uncacheable replies. Add regression coverage for non-answer refreshes, NODATA, SERVFAIL, NOTIMP, stale-window expiry, and bounded verify reply shaping. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6 Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com> * plugin/cache: validate preferred stale answers Reject truncated, DNSSEC-expired, mismatched-class, unrelated ANY, and ambiguous CNAME refreshes before replacing a stale last-known-good answer. Precompute answer eligibility when cache items are created so prefer_positive hits avoid repeated CNAME walks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6 Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com> --------- Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com> Co-authored-by: Nitin Nizhawan <nnizhawan@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6
This commit is contained in:
38
plugin/cache/handler.go
vendored
38
plugin/cache/handler.go
vendored
@@ -163,10 +163,10 @@ func (c *Cache) doRefresh(ctx context.Context, state request.Request, cw dns.Res
|
||||
|
||||
// verifyWithTimeout runs the upstream verify in a background goroutine and races it
|
||||
// against verifyStaleTimeout. If the verify completes within the timeout and the
|
||||
// response is cacheable (NoError or NXDomain), the freshly cached entry is served
|
||||
// to the client and served is true. Otherwise served is false and the caller falls
|
||||
// through to serve stale; the goroutine continues to run and any successful response
|
||||
// will update the cache without writing to the (now-detached) client connection.
|
||||
// response is accepted by verifyStaleResponseWriter, the freshly cached entry is
|
||||
// served to the client and served is true. Otherwise served is false and the caller
|
||||
// falls through to serve stale; the goroutine continues to run and any cacheable
|
||||
// response updates the cache without writing to the detached client connection.
|
||||
func (c *Cache) verifyWithTimeout(ctx context.Context, state request.Request, w dns.ResponseWriter, cw *verifyStaleResponseWriter, r *dns.Msg, do, ad bool, i *item, failureRecheck time.Duration, now func() time.Time) (served bool, code int, err error) {
|
||||
type result struct {
|
||||
code int
|
||||
@@ -191,17 +191,18 @@ func (c *Cache) verifyWithTimeout(ctx context.Context, state request.Request, w
|
||||
if !cw.refreshed {
|
||||
return false, 0, nil
|
||||
}
|
||||
fresh := c.exists(state.Name(), state.QType(), state.QClass(), state.Do(), state.Req.CheckingDisabled)
|
||||
if fresh == nil {
|
||||
// Should not happen: refreshed=true means the upstream response was cacheable.
|
||||
if cw.response == nil {
|
||||
return true, res.code, res.err
|
||||
}
|
||||
now := c.now()
|
||||
if c.keepttl {
|
||||
now = fresh.stored
|
||||
response := cw.response
|
||||
if cw.item != nil {
|
||||
now := c.now()
|
||||
if c.keepttl {
|
||||
now = cw.item.stored
|
||||
}
|
||||
response = cw.item.toMsg(r, now, do, ad)
|
||||
}
|
||||
resp := fresh.toMsg(r, now, do, ad)
|
||||
if err := w.WriteMsg(resp); err != nil {
|
||||
if err := w.WriteMsg(response); err != nil {
|
||||
return true, dns.RcodeServerFailure, err
|
||||
}
|
||||
return true, dns.RcodeSuccess, nil
|
||||
@@ -227,6 +228,19 @@ func (c *Cache) getIfNotStale(now time.Time, state request.Request, server strin
|
||||
k := hash(state.Name(), state.QType(), state.QClass(), state.Do(), state.Req.CheckingDisabled)
|
||||
cacheRequests.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
|
||||
if c.preferPositive && c.staleUpTo > 0 {
|
||||
if i, ok := c.pcache.Get(k); ok {
|
||||
i = i.answeringItem(state)
|
||||
if i != nil {
|
||||
ttl := i.ttl(now)
|
||||
if ttl > 0 || -ttl < int(c.staleUpTo.Seconds()) {
|
||||
cacheHits.WithLabelValues(server, Success, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i, ok := c.ncache.Get(k); ok {
|
||||
ttl := i.ttl(now)
|
||||
if i.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) {
|
||||
|
||||
Reference in New Issue
Block a user