Skip to content

Commit

Permalink
Merge pull request folbricht#279 from folbricht/prefetch-3
Browse files Browse the repository at this point in the history
Implement cache-prefetch
  • Loading branch information
charlieporth1 authored Jan 26, 2023
2 parents 3c6cfc0 + a1bb645 commit 0064857
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 10 deletions.
56 changes: 50 additions & 6 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ type CacheOptions struct {

// Query name that will trigger a cache flush. Disabled if empty.
FlushQuery string

// If a query is received for a record with less that PrefetchTrigger TTL left, the
// cache will send another query to upstream. The goal is to automatically refresh
// the record in the cache.
PrefetchTrigger uint32

// Only records with at least PrefetchEligible seconds TTL are eligible to be prefetched.
PrefetchEligible uint32
}

// NewCache returns a new instance of a Cache resolver.
Expand Down Expand Up @@ -107,10 +115,43 @@ func (r *Cache) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
}

// Returned an answer from the cache if one exists
a, ok := r.answerFromCache(q)
a, prefetchEligible, ok := r.answerFromCache(q)
if ok {
log.Debug("cache-hit")
r.metrics.hit.Add(1)

// If prefetch is enabled and the TTL has fallen below the trigger time, send
// a concurrent query upstream (to refresh the cached record)
if prefetchEligible && r.CacheOptions.PrefetchTrigger > 0 {
if min, ok := minTTL(a); ok && min < r.CacheOptions.PrefetchTrigger {
prefetchQ := q.Copy()
go func() {
log.Debug("prefetching record")

// Send the same query upstream
prefetchA, err := r.resolver.Resolve(prefetchQ, ci)
if err != nil || prefetchA == nil {
return
}

// Don't cache truncated responses
if prefetchA.Truncated {
return
}

// If the prefetched record has a lower TTL than what we had already, there
// is no point in storing it in the cache. This can happen when the upstream
// resolver also uses caching.
if prefetchAMin, ok := minTTL(prefetchA); !ok || prefetchAMin < min {
return
}

// Put the upstream response into the cache and return it.
r.storeInCache(prefetchQ, prefetchA)
}()
}
}

return a, nil
}
r.metrics.miss.Add(1)
Expand Down Expand Up @@ -139,16 +180,18 @@ func (r *Cache) String() string {
}

// Returns an answer from the cache with it's TTL updated or false in case of a cache-miss.
func (r *Cache) answerFromCache(q *dns.Msg) (*dns.Msg, bool) {
func (r *Cache) answerFromCache(q *dns.Msg) (*dns.Msg, bool, bool) {
var answer *dns.Msg
var timestamp time.Time
var prefetchEligible bool
r.mu.Lock()
if a := r.lru.get(q); a != nil {
if r.ShuffleAnswerFunc != nil {
r.ShuffleAnswerFunc(a.Msg)
}
answer = a.Copy()
timestamp = a.timestamp
prefetchEligible = a.prefetchEligible
}
r.mu.Unlock()

Expand All @@ -164,7 +207,7 @@ func (r *Cache) answerFromCache(q *dns.Msg) (*dns.Msg, bool) {
if a := r.lru.get(newQ); a != nil {
if a.Rcode == dns.RcodeNameError {
r.mu.Unlock()
return nxdomain(q), true
return nxdomain(q), false, true
}
break
}
Expand All @@ -174,7 +217,7 @@ func (r *Cache) answerFromCache(q *dns.Msg) (*dns.Msg, bool) {

// Return a cache-miss if there's no answer record in the map
if answer == nil {
return nil, false
return nil, false, false
}

// Make a copy of the response before returning it. Some later
Expand All @@ -197,13 +240,13 @@ func (r *Cache) answerFromCache(q *dns.Msg) (*dns.Msg, bool) {
h := a.Header()
if age >= h.Ttl {
r.evictFromCache(q)
return nil, false
return nil, false, false
}
h.Ttl -= age
}
}

return answer, true
return answer, prefetchEligible, true
}

func (r *Cache) storeInCache(query, answer *dns.Msg) {
Expand All @@ -220,6 +263,7 @@ func (r *Cache) storeInCache(query, answer *dns.Msg) {
case dns.RcodeSuccess, dns.RcodeNameError, dns.RcodeRefused, dns.RcodeNotImplemented, dns.RcodeFormatError:
if ok {
item.expiry = now.Add(time.Duration(min) * time.Second)
item.prefetchEligible = min > r.CacheOptions.PrefetchEligible
} else {
item.expiry = now.Add(time.Duration(r.NegativeTTL) * time.Second)
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/routedns/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type group struct {
CacheAnswerShuffle string `toml:"cache-answer-shuffle"` // Algorithm to use for modifying the response order of cached items
CacheHardenBelowNXDOMAIN bool `toml:"cache-harden-below-nxdomain"` // Return NXDOMAIN if an NXDOMAIN is cached for a parent domain
CacheFlushQuery string `toml:"cache-flush-query"` // Flush the cache when a query for this name is received
PrefetchTrigger uint32 `toml:"cache-prefetch-trigger"` // Prefetch when the TTL of a query has fallen below this value
PrefetchEligible uint32 `toml:"cache-prefetch-eligible"` // Only records with TTL greater than this are considered for prefetch

// Blocklist options
Blocklist []string // Blocklist rules, only used by "blocklist" type
Expand Down
16 changes: 16 additions & 0 deletions cmd/routedns/example-config/cache-with-prefetch.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Simple proxy using a cache.

[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
protocol = "dot"

[groups.cloudflare-cached]
type = "cache"
resolvers = ["cloudflare-dot"]
cache-prefetch-trigger = 10 # Prefetch when the TTL has fallen below this value
cache-prefetch-eligible = 20 # Only prefetch records if their original TTL is above this

[listeners.local-udp]
address = "127.0.0.1:53"
protocol = "udp"
resolver = "cloudflare-cached"
2 changes: 2 additions & 0 deletions cmd/routedns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,8 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
ShuffleAnswerFunc: shuffleFunc,
HardenBelowNXDOMAIN: g.CacheHardenBelowNXDOMAIN,
FlushQuery: g.CacheFlushQuery,
PrefetchTrigger: g.PrefetchTrigger,
PrefetchEligible: g.PrefetchEligible,
}
resolvers[id] = rdns.NewCache(id, gr[0], opt)
case "response-blocklist-ip", "response-blocklist-cidr": // "response-blocklist-cidr" has been retired/renamed to "response-blocklist-ip"
Expand Down
6 changes: 4 additions & 2 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,10 @@ Options:
- `cache-size` - Max number of responses to cache. Defaults to 0 which means no limit. Optional
- `cache-negative-ttl` - TTL (in seconds) to apply to responses without a SOA. Default: 60. Optional
- `cache-answer-shuffle` - Specifies a method for changing the order of cached A/AAAA answer records. Possible values `random` or `round-robin`. Defaults to static responses if not set.
- `cache-harden-below-nxdomain` - Return NXDOMAIN for sudomain queries if the parent domain has a cached NXDOMAIN. See [RFC8020](https://tools.ietf.org/html/rfc8020).
- `cache-harden-below-nxdomain` - Return NXDOMAIN for domain queries if the parent domain has a cached NXDOMAIN. See [RFC8020](https://tools.ietf.org/html/rfc8020).
- `cache-flush-query` - A query name (FQDN with trailing `.`) that if received from a client will trigger a cache flush (reset). Inactive if not set. Simple way to support flushing the cache by sending a pre-defined query name of any type. If successful, the response will be empty. The query will not be forwarded upstream by the cache.
- `cache-prefetch-trigger`- If a query is received for a record with less that `cache-prefetch-trigger` TTL left, the cache will send another, independent query to upstream with the goal of automatically refreshing the record in the cache with the response.
- `cache-prefetch-eligible` - Only records with at least `prefetch-eligible` seconds TTL are eligible to be prefetched.

#### Examples

Expand Down Expand Up @@ -334,7 +336,7 @@ resolvers = ["cloudflare-dot"]
cache-flush-query = "flush.cache."
```

Example config files: [cache.toml](../cmd/routedns/example-config/cache.toml), [block-split-cache.toml](../cmd/routedns/example-config/block-split-cache.toml), [cache-flush.toml](../cmd/routedns/example-config/cache-flush.toml)
Example config files: [cache.toml](../cmd/routedns/example-config/cache.toml), [block-split-cache.toml](../cmd/routedns/example-config/block-split-cache.toml), [cache-flush.toml](../cmd/routedns/example-config/cache-flush.toml), [cache-with-prefetch.toml](../cmd/routedns/example-config/cache-with-prefetch.toml)

### TTL modifier

Expand Down
8 changes: 6 additions & 2 deletions lru-cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ type lruKey struct {
}

type cacheAnswer struct {
timestamp time.Time // Time the record was cached. Needed to adjust TTL
expiry time.Time // Time the record expires and should be removed
timestamp time.Time // Time the record was cached. Needed to adjust TTL
expiry time.Time // Time the record expires and should be removed
prefetchEligible bool // The cache can prefetch this record
*dns.Msg
}

Expand All @@ -47,6 +48,9 @@ func (c *lruCache) add(query *dns.Msg, answer *cacheAnswer) {
key := lruKeyFromQuery(query)
item := c.touch(key)
if item != nil {
// Update the item, it's already at the top of the list
// so we can just change the value
item.cacheAnswer = answer
return
}
// Add new item to the top of the linked list
Expand Down

0 comments on commit 0064857

Please sign in to comment.