middleware/cache: split cache in positive and negative and use lru (#298)

Make the cache memory bounded, by using a LRU cache. Also split the
cache in a positive and negative one - each with its own controls.

Extend the cache stanza to allow for this:

    cache {
       positive limit [ttl]
       negative limit [ttl]
    }

is now possible. This also add a cache_test.go in the toplevel test/
directory that exercises the caching path.

Fixes #260
This commit is contained in:
Miek Gieben 2016-10-02 08:31:44 +01:00 committed by GitHub
parent 9b6b8d2762
commit e54c232c8c
16 changed files with 413 additions and 190 deletions

View file

@ -4,20 +4,32 @@
## Syntax ## Syntax
~~~ ~~~ txt
cache [ttl] [zones...] cache [ttl] [zones...]
~~~ ~~~
* `ttl` max TTL in seconds. If not specified, the TTL of the reply (SOA minimum or minimum TTL in the * `ttl` max TTL in seconds. If not specified, the maximum TTL will be used which is 1 hours for
answer section) will be used. positive responses and half an hour for negative ones.
* `zones` zones it should cache for. If empty, the zones from the configuration block are used. * `zones` zones it should cache for. If empty, the zones from the configuration block are used.
Each element in the cache is cached according to its TTL. For the negative cache, the SOA's MinTTL Each element in the cache is cached according to its TTL (with `ttl` as the max).
value is used. For the negative cache, the SOA's MinTTL value is used. A cache can contain up to 10,000 items by
default.
A cache mostly makes sense with a middleware that is potentially slow (e.g., a proxy that retrieves an Or if you want more control:
answer), or to minimize backend queries for middleware like etcd. Using a cache with the file
middleware essentially doubles the memory load with no conceivable increase of query speed. ~~~ txt
cache [ttl] [zones...] {
postive capacity [ttl]
negative capacity [ttl]
}
~~~
* `ttl` and `zones` as above.
* `positive`, override the settings for caching positive responses, capacity indicates the maximum
number of packets we cache before we start evicting (LRU). Ttl overrides the cache maximum TTL.
* `negative`, override the settings for caching negative responses, capacity indicates the maximum
number of packets we cache before we start evicting (LRU). Ttl overrides the cache maximum TTL.
The minimum TTL allowed on resource records is 5 seconds. The minimum TTL allowed on resource records is 5 seconds.

View file

@ -2,61 +2,58 @@ package cache
import ( import (
"log" "log"
"strconv"
"strings" "strings"
"time" "time"
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/pkg/response" "github.com/miekg/coredns/middleware/pkg/response"
"github.com/hashicorp/golang-lru"
"github.com/miekg/dns" "github.com/miekg/dns"
gcache "github.com/patrickmn/go-cache"
) )
// Cache is middleware that looks up responses in a cache and caches replies. // Cache is middleware that looks up responses in a cache and caches replies.
// It has a positive and a negative cache.
type Cache struct { type Cache struct {
Next middleware.Handler Next middleware.Handler
Zones []string Zones []string
cache *gcache.Cache
cap time.Duration ncache *lru.Cache
ncap int
nttl time.Duration
pcache *lru.Cache
pcap int
pttl time.Duration
} }
// NewCache returns a new cache. // Return key under which we store the item.
func NewCache(ttl int, zones []string, next middleware.Handler) Cache { func key(m *dns.Msg, t response.Type, do bool) string {
return Cache{Next: next, Zones: zones, cache: gcache.New(defaultDuration, purgeDuration), cap: time.Duration(ttl) * time.Second}
}
func cacheKey(m *dns.Msg, t response.Type, do bool) string {
if m.Truncated { if m.Truncated {
// TODO(miek): wise to store truncated responses?
return ""
}
if t == response.OtherError {
return "" return ""
} }
qtype := m.Question[0].Qtype qtype := m.Question[0].Qtype
qname := strings.ToLower(m.Question[0].Name) qname := strings.ToLower(m.Question[0].Name)
switch t { return rawKey(qname, qtype, do)
case response.Success: }
fallthrough
case response.Delegation: func rawKey(qname string, qtype uint16, do bool) string {
return successKey(qname, qtype, do) if do {
case response.NameError: return "1" + qname + "." + strconv.Itoa(int(qtype))
return nameErrorKey(qname, do)
case response.NoData:
return noDataKey(qname, qtype, do)
case response.OtherError:
return ""
} }
return "" return "0" + qname + "." + strconv.Itoa(int(qtype))
} }
// ResponseWriter is a response writer that caches the reply message. // ResponseWriter is a response writer that caches the reply message.
type ResponseWriter struct { type ResponseWriter struct {
dns.ResponseWriter dns.ResponseWriter
cache *gcache.Cache *Cache
cap time.Duration
}
// NewCachingResponseWriter returns a new ResponseWriter.
func NewCachingResponseWriter(w dns.ResponseWriter, cache *gcache.Cache, cap time.Duration) *ResponseWriter {
return &ResponseWriter{w, cache, cap}
} }
// WriteMsg implements the dns.ResponseWriter interface. // WriteMsg implements the dns.ResponseWriter interface.
@ -67,42 +64,47 @@ func (c *ResponseWriter) WriteMsg(res *dns.Msg) error {
do = opt.Do() do = opt.Do()
} }
key := cacheKey(res, mt, do) key := key(res, mt, do)
c.set(res, key, mt)
if c.cap != 0 { duration := c.pttl
setCap(res, uint32(c.cap.Seconds())) if mt == response.NameError || mt == response.NoData {
duration = c.nttl
} }
msgTTL := minMsgTTL(res, mt)
if msgTTL < duration {
duration = msgTTL
}
if key != "" {
c.set(res, key, mt, duration)
}
setMsgTTL(res, uint32(duration.Seconds()))
return c.ResponseWriter.WriteMsg(res) return c.ResponseWriter.WriteMsg(res)
} }
func (c *ResponseWriter) set(m *dns.Msg, key string, mt response.Type) { func (c *ResponseWriter) set(m *dns.Msg, key string, mt response.Type, duration time.Duration) {
if key == "" { if key == "" {
log.Printf("[ERROR] Caching called with empty cache key") log.Printf("[ERROR] Caching called with empty cache key")
return return
} }
duration := c.cap
switch mt { switch mt {
case response.Success, response.Delegation: case response.Success, response.Delegation:
if c.cap == 0 {
duration = minTTL(m.Answer, mt)
}
i := newItem(m, duration) i := newItem(m, duration)
c.pcache.Add(key, i)
c.cache.Set(key, i, duration)
case response.NameError, response.NoData: case response.NameError, response.NoData:
if c.cap == 0 {
duration = minTTL(m.Ns, mt)
}
i := newItem(m, duration) i := newItem(m, duration)
c.ncache.Add(key, i)
c.cache.Set(key, i, duration)
case response.OtherError: case response.OtherError:
// don't cache these // don't cache these
// TODO(miek): what do we do with these?
default: default:
log.Printf("[WARNING] Caching called with unknown middleware MsgType: %d", mt) log.Printf("[WARNING] Caching called with unknown classification: %d", mt)
} }
} }
@ -113,36 +115,10 @@ func (c *ResponseWriter) Write(buf []byte) (int, error) {
return n, err return n, err
} }
// Hijack implements the dns.ResponseWriter interface.
func (c *ResponseWriter) Hijack() {
c.ResponseWriter.Hijack()
return
}
func minTTL(rrs []dns.RR, mt response.Type) time.Duration {
if mt != response.Success && mt != response.NameError && mt != response.NoData {
return 0
}
minTTL := maxTTL
for _, r := range rrs {
switch mt {
case response.NameError, response.NoData:
if r.Header().Rrtype == dns.TypeSOA {
return time.Duration(r.(*dns.SOA).Minttl) * time.Second
}
case response.Success, response.Delegation:
if r.Header().Ttl < minTTL {
minTTL = r.Header().Ttl
}
}
}
return time.Duration(minTTL) * time.Second
}
const ( const (
purgeDuration = 1 * time.Minute maxTTL = 1 * time.Hour
defaultDuration = 20 * time.Minute maxNTTL = 30 * time.Minute
baseTTL = 5 // minimum TTL that we will allow minTTL = 5 * time.Second
maxTTL uint32 = 2 * 3600
defaultCap = 10000 // default capacity of the cache.
) )

View file

@ -8,6 +8,7 @@ import (
"github.com/miekg/coredns/middleware/pkg/response" "github.com/miekg/coredns/middleware/pkg/response"
"github.com/miekg/coredns/middleware/test" "github.com/miekg/coredns/middleware/test"
lru "github.com/hashicorp/golang-lru"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -26,21 +27,15 @@ var cacheTestCases = []cacheTestCase{
Case: test.Case{ Case: test.Case{
Qname: "miek.nl.", Qtype: dns.TypeMX, Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{ Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."), test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."),
}, },
}, },
in: test.Case{ in: test.Case{
Qname: "miek.nl.", Qtype: dns.TypeMX, Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{ Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), test.MX("miek.nl. 3601 IN MX 1 aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."), test.MX("miek.nl. 3601 IN MX 10 aspmx2.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."),
}, },
}, },
}, },
@ -65,14 +60,17 @@ func cacheMsg(m *dns.Msg, tc cacheTestCase) *dns.Msg {
return m return m
} }
func newTestCache() (Cache, *ResponseWriter) { func newTestCache(ttl time.Duration) (*Cache, *ResponseWriter) {
c := NewCache(0, []string{"."}, nil) c := &Cache{Zones: []string{"."}, pcap: defaultCap, ncap: defaultCap, pttl: ttl, nttl: ttl}
crr := NewCachingResponseWriter(nil, c.cache, time.Duration(0)) c.pcache, _ = lru.New(c.pcap)
c.ncache, _ = lru.New(c.ncap)
crr := &ResponseWriter{nil, c}
return c, crr return c, crr
} }
func TestCache(t *testing.T) { func TestCache(t *testing.T) {
c, crr := newTestCache() c, crr := newTestCache(maxTTL)
for _, tc := range cacheTestCases { for _, tc := range cacheTestCases {
m := tc.in.Msg() m := tc.in.Msg()
@ -80,14 +78,15 @@ func TestCache(t *testing.T) {
do := tc.in.Do do := tc.in.Do
mt, _ := response.Classify(m) mt, _ := response.Classify(m)
key := cacheKey(m, mt, do) k := key(m, mt, do)
crr.set(m, key, mt) crr.set(m, k, mt, c.pttl)
name := middleware.Name(m.Question[0].Name).Normalize() name := middleware.Name(m.Question[0].Name).Normalize()
qtype := m.Question[0].Qtype qtype := m.Question[0].Qtype
i, ok := c.get(name, qtype, do) i, ok, _ := c.get(name, qtype, do)
if !ok && !m.Truncated { if ok && m.Truncated {
t.Errorf("Truncated message should not have been cached") t.Errorf("Truncated message should not have been cached")
continue
} }
if ok { if ok {

View file

@ -1,6 +1,8 @@
package cache package cache
import ( import (
"time"
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/request" "github.com/miekg/coredns/request"
@ -10,7 +12,7 @@ import (
) )
// ServeDNS implements the middleware.Handler interface. // ServeDNS implements the middleware.Handler interface.
func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r} state := request.Request{W: w, Req: r}
qname := state.Name() qname := state.Name()
@ -20,34 +22,36 @@ func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
return c.Next.ServeDNS(ctx, w, r) return c.Next.ServeDNS(ctx, w, r)
} }
do := state.Do() // might need more from OPT record? do := state.Do() // might need more from OPT record? Like the actual bufsize?
if i, ok, expired := c.get(qname, qtype, do); ok && !expired {
if i, ok := c.get(qname, qtype, do); ok {
resp := i.toMsg(r) resp := i.toMsg(r)
state.SizeAndDo(resp) state.SizeAndDo(resp)
w.WriteMsg(resp) w.WriteMsg(resp)
cacheHitCount.WithLabelValues(zone).Inc() cacheHitCount.WithLabelValues(zone).Inc()
return dns.RcodeSuccess, nil return dns.RcodeSuccess, nil
} }
cacheMissCount.WithLabelValues(zone).Inc() cacheMissCount.WithLabelValues(zone).Inc()
crr := NewCachingResponseWriter(w, c.cache, c.cap) crr := &ResponseWriter{w, c}
return c.Next.ServeDNS(ctx, crr, r) return c.Next.ServeDNS(ctx, crr, r)
} }
func (c Cache) get(qname string, qtype uint16, do bool) (*item, bool) { func (c *Cache) get(qname string, qtype uint16, do bool) (*item, bool, bool) {
nxdomain := nameErrorKey(qname, do) k := rawKey(qname, qtype, do)
if i, ok := c.cache.Get(nxdomain); ok {
return i.(*item), true if i, ok := c.ncache.Get(k); ok {
return i.(*item), ok, i.(*item).expired(time.Now())
} }
// TODO(miek): delegation was added double check if i, ok := c.pcache.Get(k); ok {
successOrNoData := successKey(qname, qtype, do) return i.(*item), ok, i.(*item).expired(time.Now())
if i, ok := c.cache.Get(successOrNoData); ok {
return i.(*item), true
} }
return nil, false return nil, false, false
} }
var ( var (

View file

@ -1,9 +1,9 @@
package cache package cache
import ( import (
"strconv"
"time" "time"
"github.com/miekg/coredns/middleware/pkg/response"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -44,7 +44,7 @@ func newItem(m *dns.Msg, d time.Duration) *item {
return i return i
} }
// toMsg turns i into a message, it tailers to reply to m. // toMsg turns i into a message, it tailers the reply to m.
func (i *item) toMsg(m *dns.Msg) *dns.Msg { func (i *item) toMsg(m *dns.Msg) *dns.Msg {
m1 := new(dns.Msg) m1 := new(dns.Msg)
m1.SetReply(m) m1.SetReply(m)
@ -58,44 +58,51 @@ func (i *item) toMsg(m *dns.Msg) *dns.Msg {
m1.Extra = i.Extra m1.Extra = i.Extra
ttl := int(i.origTTL) - int(time.Now().UTC().Sub(i.stored).Seconds()) ttl := int(i.origTTL) - int(time.Now().UTC().Sub(i.stored).Seconds())
if ttl < baseTTL { if ttl < int(minTTL.Seconds()) {
ttl = baseTTL ttl = int(minTTL.Seconds())
} }
setCap(m1, uint32(ttl)) setMsgTTL(m1, uint32(ttl))
return m1 return m1
} }
// setCap sets the ttl on all RRs in all sections. func (i *item) expired(now time.Time) bool {
func setCap(m *dns.Msg, ttl uint32) { ttl := int(i.origTTL) - int(now.UTC().Sub(i.stored).Seconds())
return ttl < 0
}
// setMsgTTL sets the ttl on all RRs in all sections.
func setMsgTTL(m *dns.Msg, ttl uint32) {
for _, r := range m.Answer { for _, r := range m.Answer {
r.Header().Ttl = uint32(ttl) r.Header().Ttl = ttl
} }
for _, r := range m.Ns { for _, r := range m.Ns {
r.Header().Ttl = uint32(ttl) r.Header().Ttl = ttl
} }
for _, r := range m.Extra { for _, r := range m.Extra {
if r.Header().Rrtype == dns.TypeOPT { if r.Header().Rrtype == dns.TypeOPT {
continue continue
} }
r.Header().Ttl = uint32(ttl) r.Header().Ttl = ttl
} }
} }
// nodataKey returns a caching key for NODATA responses. func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration {
func noDataKey(qname string, qtype uint16, do bool) string { if mt != response.Success && mt != response.NameError && mt != response.NoData {
if do { return 0
return "1" + qname + ".." + strconv.Itoa(int(qtype))
} }
return "0" + qname + ".." + strconv.Itoa(int(qtype))
}
// nameErrorKey returns a caching key for NXDOMAIN responses. minTTL := maxTTL
func nameErrorKey(qname string, do bool) string { for _, r := range append(m.Answer, m.Ns...) {
if do { switch mt {
return "1" + qname case response.NameError, response.NoData:
if r.Header().Rrtype == dns.TypeSOA {
return time.Duration(r.(*dns.SOA).Minttl) * time.Second
}
case response.Success, response.Delegation:
if r.Header().Ttl < uint32(minTTL.Seconds()) {
minTTL = time.Duration(r.Header().Ttl) * time.Second
}
}
} }
return "0" + qname return minTTL
} }
// successKey returns a caching key for successfull answers.
func successKey(qname string, qtype uint16, do bool) string { return noDataKey(qname, qtype, do) }

View file

@ -7,19 +7,14 @@ import (
) )
func TestKey(t *testing.T) { func TestKey(t *testing.T) {
if noDataKey("miek.nl.", dns.TypeMX, false) != "0miek.nl...15" { if x := rawKey("miek.nl.", dns.TypeMX, false); x != "0miek.nl..15" {
t.Errorf("failed to create correct key") t.Errorf("failed to create correct key, got %s", x)
} }
if noDataKey("miek.nl.", dns.TypeMX, true) != "1miek.nl...15" { if x := rawKey("miek.nl.", dns.TypeMX, true); x != "1miek.nl..15" {
t.Errorf("failed to create correct key") t.Errorf("failed to create correct key, got %s", x)
} }
if nameErrorKey("miek.nl.", false) != "0miek.nl." { // rawKey does not lowercase.
t.Errorf("failed to create correct key") if x := rawKey("miEK.nL.", dns.TypeMX, true); x != "1miEK.nL..15" {
} t.Errorf("failed to create correct key, got %s", x)
if nameErrorKey("miek.nl.", true) != "1miek.nl." {
t.Errorf("failed to create correct key")
}
if noDataKey("miek.nl.", dns.TypeMX, false) != successKey("miek.nl.", dns.TypeMX, false) {
t.Errorf("nameErrorKey and successKey should be the same")
} }
} }

View file

@ -2,10 +2,12 @@ package cache
import ( import (
"strconv" "strconv"
"time"
"github.com/miekg/coredns/core/dnsserver" "github.com/miekg/coredns/core/dnsserver"
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/hashicorp/golang-lru"
"github.com/mholt/caddy" "github.com/mholt/caddy"
) )
@ -16,51 +18,103 @@ func init() {
}) })
} }
// Cache sets up the root file path of the server.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
ttl, zones, err := cacheParse(c) ca, err := cacheParse(c)
if err != nil { if err != nil {
return middleware.Error("cache", err) return middleware.Error("cache", err)
} }
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler { dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
return NewCache(ttl, zones, next) ca.Next = next
return ca
}) })
return nil return nil
} }
func cacheParse(c *caddy.Controller) (int, []string, error) { func cacheParse(c *caddy.Controller) (*Cache, error) {
var (
err error ca := &Cache{pcap: defaultCap, ncap: defaultCap, pttl: maxTTL, nttl: maxNTTL}
ttl int
origins []string
)
for c.Next() { for c.Next() {
if c.Val() == "cache" { // cache [ttl] [zones..]
// cache [ttl] [zones..] origins := make([]string, len(c.ServerBlockKeys))
origins = make([]string, len(c.ServerBlockKeys)) copy(origins, c.ServerBlockKeys)
copy(origins, c.ServerBlockKeys) args := c.RemainingArgs()
args := c.RemainingArgs()
if len(args) > 0 {
origins = args
// first args may be just a number, then it is the ttl, if not it is a zone
t := origins[0]
ttl, err = strconv.Atoi(t)
if err == nil {
origins = origins[1:]
if len(origins) == 0 {
// There was *only* the ttl, revert back to server block
copy(origins, c.ServerBlockKeys)
}
}
}
for i := range origins { if len(args) > 0 {
origins[i] = middleware.Host(origins[i]).Normalize() // first args may be just a number, then it is the ttl, if not it is a zone
ttl, err := strconv.Atoi(args[0])
if err == nil {
ca.pttl = time.Duration(ttl) * time.Second
ca.nttl = time.Duration(ttl) * time.Second
args = args[1:]
}
if len(args) > 0 {
copy(origins, args)
} }
return ttl, origins, nil
} }
// Refinements? In an extra block.
for c.NextBlock() {
switch c.Val() {
// first number is cap, second is an new ttl
case "positive":
args := c.RemainingArgs()
if len(args) == 0 {
return nil, c.ArgErr()
}
pcap, err := strconv.Atoi(args[0])
if err != nil {
return nil, err
}
ca.pcap = pcap
if len(args) > 1 {
pttl, err := strconv.Atoi(args[1])
if err != nil {
return nil, err
}
ca.pttl = time.Duration(pttl) * time.Second
}
case "negative":
args := c.RemainingArgs()
if len(args) == 0 {
return nil, c.ArgErr()
}
ncap, err := strconv.Atoi(args[0])
if err != nil {
return nil, err
}
ca.ncap = ncap
if len(args) > 1 {
nttl, err := strconv.Atoi(args[1])
if err != nil {
return nil, err
}
ca.nttl = time.Duration(nttl) * time.Second
}
default:
return nil, c.ArgErr()
}
}
for i := range origins {
origins[i] = middleware.Host(origins[i]).Normalize()
}
var err error
ca.Zones = origins
ca.pcache, err = lru.New(ca.pcap)
if err != nil {
return nil, err
}
ca.ncache, err = lru.New(ca.ncap)
if err != nil {
return nil, err
}
return ca, nil
} }
return 0, nil, nil
return nil, nil
} }

71
middleware/cache/setup_test.go vendored Normal file
View file

@ -0,0 +1,71 @@
package cache
import (
"testing"
"time"
"github.com/mholt/caddy"
)
func TestSetup(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedNcap int
expectedPcap int
expectedNttl time.Duration
expectedPttl time.Duration
}{
{`cache`, false, defaultCap, defaultCap, maxNTTL, maxTTL},
{`cache {}`, false, defaultCap, defaultCap, maxNTTL, maxTTL},
{`cache example.nl {
positive 10
}`, false, defaultCap, 10, maxNTTL, maxTTL},
{`cache example.nl {
positive 10
negative 10 15
}`, false, 10, 10, 15 * time.Second, maxTTL},
{`cache 25 example.nl {
positive 10
negative 10 15
}`, false, 10, 10, 15 * time.Second, 25 * time.Second},
{`cache aaa example.nl`, false, defaultCap, defaultCap, maxNTTL, maxTTL},
// fails
{`cache example.nl {
positive
negative 10 15
}`, true, defaultCap, defaultCap, maxTTL, maxTTL},
{`cache example.nl {
positive 15
negative aaa
}`, true, defaultCap, defaultCap, maxTTL, maxTTL},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
ca, err := cacheParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %v: Expected error but found nil", i)
continue
} else if !test.shouldErr && err != nil {
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
continue
}
if test.shouldErr && err != nil {
continue
}
if ca.ncap != test.expectedNcap {
t.Errorf("Test %v: Expected ncap %v but found: %v", i, test.expectedNcap, ca.ncap)
}
if ca.pcap != test.expectedPcap {
t.Errorf("Test %v: Expected pcap %v but found: %v", i, test.expectedPcap, ca.pcap)
}
if ca.nttl != test.expectedNttl {
t.Errorf("Test %v: Expected nttl %v but found: %v", i, test.expectedNttl, ca.nttl)
}
if ca.pttl != test.expectedPttl {
t.Errorf("Test %v: Expected pttl %v but found: %v", i, test.expectedPttl, ca.pttl)
}
}
}

View file

@ -18,6 +18,22 @@ const (
OtherError OtherError
) )
func (t Type) String() string {
switch t {
case Success:
return "NOERROR"
case NameError:
return "NXDOMAIN"
case NoData:
return "NODATA"
case Delegation:
return "DELEGATION"
case OtherError:
return "OTHERERROR"
}
return ""
}
// Classify classifies a message, it returns the Type. // Classify classifies a message, it returns the Type.
func Classify(m *dns.Msg) (Type, *dns.OPT) { func Classify(m *dns.Msg) (Type, *dns.OPT) {
opt := m.IsEdns0() opt := m.IsEdns0()

89
test/cache_test.go Normal file
View file

@ -0,0 +1,89 @@
package test
import (
"io/ioutil"
"log"
"testing"
"time"
"github.com/miekg/coredns/middleware/proxy"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/coredns/request"
"github.com/miekg/dns"
)
// This tests uses the exampleOrg zone as defined in proxy_test.go
func TestLookupCache(t *testing.T) {
// Start auth. CoreDNS holding the auth zone.
name, rm, err := test.TempFile(t, ".", exampleOrg)
if err != nil {
t.Fatalf("failed to created zone: %s", err)
}
defer rm()
corefile := `example.org:0 {
file ` + name + `
}
`
i, err := CoreDNSServer(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
udp, _ := CoreDNSServerPorts(i, 0)
if udp == "" {
t.Fatalf("Could not get UDP listening port")
}
defer i.Stop()
// Start caching proxy CoreDNS that we want to test.
corefile = `example.org:0 {
proxy . ` + udp + `
cache
}
`
i, err = CoreDNSServer(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
udp, _ = CoreDNSServerPorts(i, 0)
if udp == "" {
t.Fatalf("Could not get UDP listening port")
}
defer i.Stop()
log.SetOutput(ioutil.Discard)
p := proxy.New([]string{udp})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
resp, err := p.Lookup(state, "example.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
// expect answer section with A record in it
if len(resp.Answer) == 0 {
t.Error("Expected to at least one RR in the answer section, got none")
}
ttl := resp.Answer[0].Header().Ttl
time.Sleep(2 * time.Second) // TODO(miek): meh.
resp, err = p.Lookup(state, "example.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
// expect answer section with A record in it
if len(resp.Answer) == 0 {
t.Error("Expected to at least one RR in the answer section, got none")
}
newTTL := resp.Answer[0].Header().Ttl
if newTTL >= ttl {
t.Errorf("Expected TTL to be lower than: %d, got %d", ttl, newTTL)
}
}

View file

@ -48,12 +48,12 @@ func TestEtcdStubAndProxyLookup(t *testing.T) {
ex, err := CoreDNSServer(corefile) ex, err := CoreDNSServer(corefile)
if err != nil { if err != nil {
t.Fatalf("could not get CoreDNS serving instance: %s", err) t.Fatalf("Could not get CoreDNS serving instance: %s", err)
} }
udp, _ := CoreDNSServerPorts(ex, 0) udp, _ := CoreDNSServerPorts(ex, 0)
if udp == "" { if udp == "" {
t.Fatalf("could not get udp listening port") t.Fatalf("Could not get UDP listening port")
} }
defer ex.Stop() defer ex.Stop()

View file

@ -74,12 +74,12 @@ func TestKubernetesIntegration(t *testing.T) {
func createTestServer(t *testing.T, corefile string) (*caddy.Instance, string) { func createTestServer(t *testing.T, corefile string) (*caddy.Instance, string) {
server, err := CoreDNSServer(corefile) server, err := CoreDNSServer(corefile)
if err != nil { if err != nil {
t.Fatalf("could not get CoreDNS serving instance: %s", err) t.Fatalf("Could not get CoreDNS serving instance: %s", err)
} }
udp, _ := CoreDNSServerPorts(server, 0) udp, _ := CoreDNSServerPorts(server, 0)
if udp == "" { if udp == "" {
t.Fatalf("could not get udp listening port") t.Fatalf("Could not get UDP listening port")
} }
return server, udp return server, udp

View file

@ -31,7 +31,7 @@ func TestLookupBalanceRewriteCacheDnssec(t *testing.T) {
` `
ex, err := CoreDNSServer(corefile) ex, err := CoreDNSServer(corefile)
if err != nil { if err != nil {
t.Fatalf("could not get CoreDNS serving instance: %s", err) t.Fatalf("Could not get CoreDNS serving instance: %s", err)
} }
udp, _ := CoreDNSServerPorts(ex, 0) udp, _ := CoreDNSServerPorts(ex, 0)

View file

@ -27,7 +27,7 @@ func benchmarkLookupBalanceRewriteCache(b *testing.B) {
ex, err := CoreDNSServer(corefile) ex, err := CoreDNSServer(corefile)
if err != nil { if err != nil {
t.Fatalf("could not get CoreDNS serving instance: %s", err) t.Fatalf("Could not get CoreDNS serving instance: %s", err)
} }
udp, _ := CoreDNSServerPorts(ex, 0) udp, _ := CoreDNSServerPorts(ex, 0)
defer ex.Stop() defer ex.Stop()

View file

@ -34,12 +34,12 @@ func TestLookupProxy(t *testing.T) {
i, err := CoreDNSServer(corefile) i, err := CoreDNSServer(corefile)
if err != nil { if err != nil {
t.Fatalf("could not get CoreDNS serving instance: %s", err) t.Fatalf("Could not get CoreDNS serving instance: %s", err)
} }
udp, _ := CoreDNSServerPorts(i, 0) udp, _ := CoreDNSServerPorts(i, 0)
if udp == "" { if udp == "" {
t.Fatalf("could not get udp listening port") t.Fatalf("Could not get UDP listening port")
} }
defer i.Stop() defer i.Stop()

View file

@ -14,7 +14,7 @@ func TestProxyToChaosServer(t *testing.T) {
` `
chaos, err := CoreDNSServer(corefile) chaos, err := CoreDNSServer(corefile)
if err != nil { if err != nil {
t.Fatalf("could not get CoreDNS serving instance: %s", err) t.Fatalf("Could not get CoreDNS serving instance: %s", err)
} }
udpChaos, _ := CoreDNSServerPorts(chaos, 0) udpChaos, _ := CoreDNSServerPorts(chaos, 0)
@ -26,7 +26,7 @@ func TestProxyToChaosServer(t *testing.T) {
` `
proxy, err := CoreDNSServer(corefileProxy) proxy, err := CoreDNSServer(corefileProxy)
if err != nil { if err != nil {
t.Fatalf("could not get CoreDNS serving instance") t.Fatalf("Could not get CoreDNS serving instance")
} }
udp, _ := CoreDNSServerPorts(proxy, 0) udp, _ := CoreDNSServerPorts(proxy, 0)