Skip to content

Commit

Permalink
Add a high-level DNS change test
Browse files Browse the repository at this point in the history
This more or less reproduces the scenario described in grafana#726.
  • Loading branch information
Ivan Mirić committed Aug 20, 2020
1 parent f6605d1 commit ad8d23c
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 119 deletions.
14 changes: 9 additions & 5 deletions js/initcontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/consts"
"github.com/loadimpact/k6/lib/netext"
"github.com/loadimpact/k6/lib/testutils"
"github.com/loadimpact/k6/stats"
)

Expand Down Expand Up @@ -382,16 +383,19 @@ func TestRequestWithBinaryFile(t *testing.T) {
logger.Level = logrus.DebugLevel
logger.Out = ioutil.Discard

resolver := netext.NewResolver(testutils.NewMockResolver(nil))
dialer := netext.NewDialer(net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 60 * time.Second,
DualStack: true,
}, resolver)

state := &lib.State{
Options: lib.Options{},
Logger: logger,
Group: root,
Transport: &http.Transport{
DialContext: (netext.NewDialer(net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 60 * time.Second,
DualStack: true,
})).DialContext,
DialContext: dialer.DialContext,
},
BPool: bpool.NewBufferPool(1),
Samples: make(chan stats.SampleContainer, 500),
Expand Down
4 changes: 2 additions & 2 deletions js/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type Runner struct {
defaultGroup *lib.Group

BaseDialer net.Dialer
Resolver netext.Resolver
Resolver *netext.Resolver
RPSLimit *rate.Limiter

console *console
Expand Down Expand Up @@ -99,7 +99,7 @@ func NewFromBundle(b *Bundle) (*Runner, error) {
DualStack: true,
},
console: newConsole(),
Resolver: netext.NewResolver(),
Resolver: netext.NewResolver(nil),
}

err = r.SetOptions(r.Bundle.Options)
Expand Down
51 changes: 51 additions & 0 deletions js/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
"github.com/loadimpact/k6/lib"
_ "github.com/loadimpact/k6/lib/executor" // TODO: figure out something better
"github.com/loadimpact/k6/lib/metrics"
"github.com/loadimpact/k6/lib/testutils"
"github.com/loadimpact/k6/lib/testutils/httpmultibin"
"github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/stats"
Expand Down Expand Up @@ -1665,3 +1666,53 @@ func TestSystemTags(t *testing.T) {
})
}
}

func TestDNSCache(t *testing.T) {
tb := httpmultibin.NewHTTPMultiBin(t)
defer tb.Cleanup()
sr := tb.Replacer.Replace

runner, err := getSimpleRunner("/script.js", tb.Replacer.Replace(`
var http = require("k6/http");
var url = "HTTPBIN_URL/";
exports.default = function() {
var res = http.get(url);
if (res.status != 200) { throw new Error("wrong status: " + res.status); }
}
`))
if !assert.NoError(t, err) {
return
}
runner.SetOptions(lib.Options{
Throw: null.BoolFrom(true),
MaxRedirects: null.IntFrom(10),
// NoConnectionReuse: null.BoolFrom(true),
})

resolver := testutils.NewMockResolver(nil)
samples := make(chan stats.SampleContainer, 100)

runVU := func(rr string, expErr string) {
resolver.SetRR("httpbin.local.", rr)
initVU, err := runner.NewVU(1, samples)
// Replace the VU base resolver so that the DNS change can take effect.
(initVU.(*VU)).Dialer.Resolver.BaseResolver = resolver
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
vu := initVU.Activate(&lib.VUActivationParams{RunContext: ctx})
err = vu.RunOnce()
if expErr != "" {
assert.Contains(t, err.Error(), expErr)
} else {
assert.NoError(t, err)
}
}

runVU("0 IN A 127.0.0.1", "")
// Needs to be high enough for the connection to time out(?).
// If NoConnectionReuse is specified, RunOnce() doesn't return an error. :-/
time.Sleep(5 * time.Second)
runVU("0 IN A 127.0.0.254",
sr("dial tcp 127.0.0.254:HTTPBIN_PORT: connect: connection refused"))
}
4 changes: 2 additions & 2 deletions lib/netext/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import (
type Dialer struct {
net.Dialer
ctx context.Context
Resolver Resolver
Resolver *Resolver
Blacklist []*lib.IPNet
Hosts map[string]net.IP

Expand All @@ -47,7 +47,7 @@ type Dialer struct {
}

// NewDialer constructs a new custom Dialer that wraps dialer and uses resolver.
func NewDialer(dialer net.Dialer, resolver Resolver) *Dialer {
func NewDialer(dialer net.Dialer, resolver *Resolver) *Dialer {
return &Dialer{Dialer: dialer, Resolver: resolver}
}

Expand Down
63 changes: 31 additions & 32 deletions lib/netext/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,14 @@ var (
}
)

// Resolver is the public DNS resolution interface.
type Resolver interface {
Resolve(ctx context.Context, host string, depth uint8) (net.IP, error)
// BaseResolver is the low level DNS resolution interface.
type BaseResolver interface {
Resolve(context.Context, *dns.Msg) (*dns.Msg, error)
}

// baseResolver is an internal interface used to mock out the underlying
// resolver in tests.
type baseResolver interface {
resolve(context.Context, *dns.Msg) (*dns.Msg, error)
}

// NewResolver returns a new DNS resolver with a preconfigured cache.
func NewResolver() Resolver {
// NewResolver returns a new DNS resolver with a preconfigured TTL-based cache
// using the given base resolver. If nil, the sdns resolver will be used.
func NewResolver(base BaseResolver) *Resolver {
cfg := new(config.Config)
cfg.RootServers = nameservers
// TODO: Make this configurable?
Expand All @@ -66,21 +61,25 @@ func NewResolver() Resolver {
cfg.CacheSize = 1024
cfg.Timeout.Duration = 2 * time.Second

return &resolver{
baseResolver: newSdnsResolver(cfg),
if base == nil {
base = newSdnsResolver(cfg)
}

return &Resolver{
BaseResolver: base,
cache: cachem.New(cfg),
ip4: make(map[string]bool),
cname: make(map[string]canonicalName),
}
}

type resolver struct {
baseResolver baseResolver
ctx context.Context
authservers *authcache.AuthServers
cache *cachem.Cache
ip4 map[string]bool // IPv4 last seen
cname map[string]canonicalName
type Resolver struct {
BaseResolver
ctx context.Context
authservers *authcache.AuthServers
cache *cachem.Cache
ip4 map[string]bool // IPv4 last seen
cname map[string]canonicalName
}

// canonicalName is an expiring CNAME value.
Expand All @@ -95,7 +94,7 @@ type sdnsResolver struct {
authservers *authcache.AuthServers
}

func newSdnsResolver(cfg *config.Config) baseResolver {
func newSdnsResolver(cfg *config.Config) BaseResolver {
authservers := &authcache.AuthServers{}
authservers.Zone = "." // should this be dynamic?
for _, ns := range nameservers {
Expand All @@ -111,14 +110,14 @@ func newSdnsResolver(cfg *config.Config) baseResolver {
}
}

func (r *sdnsResolver) resolve(ctx context.Context, req *dns.Msg) (*dns.Msg, error) {
func (r *sdnsResolver) Resolve(ctx context.Context, req *dns.Msg) (*dns.Msg, error) {
return r.Resolver.Resolve(ctx, req, r.authservers, false, 30, 0, false, nil)
}

// Resolve maps a host string to an IP address.
// Host string may be an IP address string or a domain name.
// Follows CNAME chain up to depth steps.
func (r *resolver) Resolve(ctx context.Context, host string, depth uint8) (net.IP, error) {
func (r *Resolver) Resolve(ctx context.Context, host string, depth uint8) (net.IP, error) {
r.ctx = ctx
ip := net.ParseIP(host)
if ip != nil {
Expand All @@ -136,7 +135,7 @@ func (r *resolver) Resolve(ctx context.Context, host string, depth uint8) (net.I
// Prefers IPv4 if last resolution produced it.
// Otherwise prefers IPv6.
// Package config constrains to only IP versions available on the system.
func (r *resolver) lookup(host string) (net.IP, dns.RR, error) {
func (r *Resolver) lookup(host string) (net.IP, dns.RR, error) {
if ip6 && ip4 {
// Both versions available
if r.ip4[host] {
Expand Down Expand Up @@ -179,7 +178,7 @@ func (r *resolver) lookup(host string) (net.IP, dns.RR, error) {
// lookup64 performs a single lookup preferring IPv6.
// Used on first resolution, if last resolution failed,
// or if last resolution produced IPv6.
func (r *resolver) lookup64(host string) (net.IP, dns.RR, error) {
func (r *Resolver) lookup64(host string) (net.IP, dns.RR, error) {
ip, cname, err := r.lookup6(host)
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -207,7 +206,7 @@ func (r *resolver) lookup64(host string) (net.IP, dns.RR, error) {
// lookup46 performs a single lookup preferring IPv4.
// Used if last resolution produced IPv4.
// Prevents hitting network looking for IPv6 for names with only IPv4.
func (r *resolver) lookup46(host string) (net.IP, dns.RR, error) {
func (r *Resolver) lookup46(host string) (net.IP, dns.RR, error) {
ip, cname, err := r.lookup4(host)
if err != nil {
return nil, nil, err
Expand All @@ -233,12 +232,12 @@ func (r *resolver) lookup46(host string) (net.IP, dns.RR, error) {
}

// lookup6 performs a single lookup for IPv6.
func (r *resolver) lookup6(host string) (net.IP, dns.RR, error) {
func (r *Resolver) lookup6(host string) (net.IP, dns.RR, error) {
req := makeReq(host, dns.TypeA)
key := cache.Hash(req.Question[0])
resp, _, err := r.cache.GetP(key, req)
if resp == nil || err != nil {
resp, err = r.baseResolver.resolve(r.ctx, req)
resp, err = r.BaseResolver.Resolve(r.ctx, req)
if resp != nil {
r.cache.Set(key, resp)
}
Expand All @@ -259,12 +258,12 @@ func (r *resolver) lookup6(host string) (net.IP, dns.RR, error) {
}

// lookup4 performs a single lookup for IPv4.
func (r *resolver) lookup4(host string) (net.IP, dns.RR, error) {
func (r *Resolver) lookup4(host string) (net.IP, dns.RR, error) {
req := makeReq(host, dns.TypeA)
key := cache.Hash(req.Question[0])
resp, _, err := r.cache.GetP(key, req)
if resp == nil || err != nil {
resp, err = r.baseResolver.resolve(r.ctx, req)
resp, err = r.BaseResolver.Resolve(r.ctx, req)
if resp != nil {
r.cache.Set(key, resp)
}
Expand All @@ -287,7 +286,7 @@ func (r *resolver) lookup4(host string) (net.IP, dns.RR, error) {
// resolveName maps a domain name to an IP address.
// Follows CNAME chain up to depth steps.
// Fails on CNAME chain cycle.
func (r *resolver) resolveName(
func (r *Resolver) resolveName(
requested string,
name string,
depth uint8,
Expand Down Expand Up @@ -337,7 +336,7 @@ func (r *resolver) resolveName(
// Follows CNAME chain up to depth steps.
// Purges expired CNAME entries.
// Fails on a cycle.
func (r *resolver) canonicalName(name string, depth uint8) (string, error) {
func (r *Resolver) canonicalName(name string, depth uint8) (string, error) {
cname := normalName(name)
observed := make(map[string]struct{})
observed[cname] = struct{}{}
Expand Down
Loading

0 comments on commit ad8d23c

Please sign in to comment.