diff --git a/cmd/config.go b/cmd/config.go index 54c7cddbd22..de37f6a44bf 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -35,6 +35,7 @@ import ( "github.com/loadimpact/k6/lib" "github.com/loadimpact/k6/lib/executor" + "github.com/loadimpact/k6/lib/types" "github.com/loadimpact/k6/stats" "github.com/loadimpact/k6/stats/cloud" "github.com/loadimpact/k6/stats/csv" @@ -259,6 +260,17 @@ func applyDefault(conf Config) Config { if conf.Options.SummaryTrendStats == nil { conf.Options.SummaryTrendStats = lib.DefaultSummaryTrendStats } + defDNS := types.DefaultDNSConfig() + if !conf.DNS.TTL.Valid { + conf.DNS.TTL = defDNS.TTL + } + if !conf.DNS.Select.Valid { + conf.DNS.Select = defDNS.Select + } + if !conf.DNS.Policy.Valid { + conf.DNS.Policy = defDNS.Policy + } + return conf } diff --git a/cmd/config_consolidation_test.go b/cmd/config_consolidation_test.go index b11763c7197..40e01e88ff0 100644 --- a/cmd/config_consolidation_test.go +++ b/cmd/config_consolidation_test.go @@ -139,7 +139,7 @@ type file struct { func getFS(files []file) afero.Fs { fs := afero.NewMemMapFs() for _, f := range files { - must(afero.WriteFile(fs, f.filepath, []byte(f.contents), 0644)) // modes don't matter in the afero.MemMapFs + must(afero.WriteFile(fs, f.filepath, []byte(f.contents), 0o644)) // modes don't matter in the afero.MemMapFs } return fs } @@ -214,11 +214,13 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase { {opts{cli: []string{"-u", "3", "-d", "30s"}}, exp{}, verifyConstLoopingVUs(I(3), 30*time.Second)}, {opts{cli: []string{"-u", "4", "--duration", "60s"}}, exp{}, verifyConstLoopingVUs(I(4), 1*time.Minute)}, { - opts{cli: []string{"--stage", "20s:10", "-s", "3m:5"}}, exp{}, + opts{cli: []string{"--stage", "20s:10", "-s", "3m:5"}}, + exp{}, verifyRampingVUs(null.NewInt(1, false), buildStages(20, 10, 180, 5)), }, { - opts{cli: []string{"-s", "1m6s:5", "--vus", "10"}}, exp{}, + opts{cli: []string{"-s", "1m6s:5", "--vus", "10"}}, + exp{}, verifyRampingVUs(null.NewInt(10, true), buildStages(66, 5)), }, {opts{cli: []string{"-u", "1", "-i", "6", "-d", "10s"}}, exp{}, func(t *testing.T, c Config) { @@ -248,11 +250,13 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase { {opts{env: []string{"K6_VUS=5", "K6_ITERATIONS=15"}}, exp{}, verifySharedIters(I(5), I(15))}, {opts{env: []string{"K6_VUS=10", "K6_DURATION=20s"}}, exp{}, verifyConstLoopingVUs(I(10), 20*time.Second)}, { - opts{env: []string{"K6_STAGES=2m30s:11,1h1m:100"}}, exp{}, + opts{env: []string{"K6_STAGES=2m30s:11,1h1m:100"}}, + exp{}, verifyRampingVUs(null.NewInt(1, false), buildStages(150, 11, 3660, 100)), }, { - opts{env: []string{"K6_STAGES=100s:100,0m30s:0", "K6_VUS=0"}}, exp{}, + opts{env: []string{"K6_STAGES=100s:100,0m30s:0", "K6_VUS=0"}}, + exp{}, verifyRampingVUs(null.NewInt(0, true), buildStages(100, 100, 30, 0)), }, // Test if JSON configs work as expected @@ -275,14 +279,16 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase { env: []string{"K6_DURATION=15s"}, cli: []string{"--stage", ""}, }, - exp{logWarning: true}, verifyOneIterPerOneVU, + exp{logWarning: true}, + verifyOneIterPerOneVU, }, { opts{ runner: &lib.Options{VUs: null.IntFrom(5), Duration: types.NullDurationFrom(50 * time.Second)}, cli: []string{"--stage", "5s:5"}, }, - exp{}, verifyRampingVUs(I(5), buildStages(5, 5)), + exp{}, + verifyRampingVUs(I(5), buildStages(5, 5)), }, { opts{ @@ -323,7 +329,8 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase { env: []string{"K6_ITERATIONS=25"}, cli: []string{"--vus", "12"}, }, - exp{}, verifySharedIters(I(12), I(25)), + exp{}, + verifySharedIters(I(12), I(25)), }, // TODO: test the externally controlled executor @@ -375,6 +382,86 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase { assert.Equal(t, []string{"avg", "p(90)", "count"}, c.Options.SummaryTrendStats) }, }, + {opts{cli: []string{}}, exp{}, func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.NewString("5m", false), + Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false}, + }, c.Options.DNS) + }}, + {opts{env: []string{"K6_DNS=ttl=5,select=roundRobin"}}, exp{}, func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.StringFrom("5"), + Select: types.NullDNSSelect{DNSSelect: types.DNSroundRobin, Valid: true}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false}, + }, c.Options.DNS) + }}, + {opts{env: []string{"K6_DNS=ttl=inf,select=random,policy=preferIPv6"}}, exp{}, func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.StringFrom("inf"), + Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: true}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv6, Valid: true}, + }, c.Options.DNS) + }}, + // This is functionally invalid, but will error out in validation done in js.parseTTL(). + {opts{cli: []string{"--dns", "ttl=-1"}}, exp{}, func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.StringFrom("-1"), + Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false}, + }, c.Options.DNS) + }}, + {opts{cli: []string{"--dns", "ttl=0,blah=nope"}}, exp{cliReadError: true}, nil}, + {opts{cli: []string{"--dns", "ttl=0"}}, exp{}, func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.StringFrom("0"), + Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false}, + }, c.Options.DNS) + }}, + {opts{cli: []string{"--dns", "ttl=5s,select="}}, exp{cliReadError: true}, nil}, + { + opts{fs: defaultConfig(`{"dns": {"ttl": "0", "select": "roundRobin", "policy": "onlyIPv4"}}`)}, + exp{}, + func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.StringFrom("0"), + Select: types.NullDNSSelect{DNSSelect: types.DNSroundRobin, Valid: true}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSonlyIPv4, Valid: true}, + }, c.Options.DNS) + }, + }, + { + opts{ + fs: defaultConfig(`{"dns": {"ttl": "0"}}`), + env: []string{"K6_DNS=ttl=30,policy=any"}, + }, + exp{}, + func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.StringFrom("30"), + Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSany, Valid: true}, + }, c.Options.DNS) + }, + }, + { + // CLI overrides all, falling back to env + opts{ + fs: defaultConfig(`{"dns": {"ttl": "60", "select": "first"}}`), + env: []string{"K6_DNS=ttl=30,select=random,policy=any"}, + cli: []string{"--dns", "ttl=5"}, + }, + exp{}, + func(t *testing.T, c Config) { + assert.Equal(t, types.DNSConfig{ + TTL: null.StringFrom("5"), + Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: true}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSany, Valid: true}, + }, c.Options.DNS) + }, + }, + // TODO: test for differences between flagsets // TODO: more tests in general, especially ones not related to execution parameters... } diff --git a/cmd/options.go b/cmd/options.go index 17bf1b477ca..0276b4daaab 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -93,6 +93,11 @@ func optionFlagSet() *pflag.FlagSet { flags.StringSlice("tag", nil, "add a `tag` to be applied to all samples, as `[name]=[value]`") flags.String("console-output", "", "redirects the console logging to the provided output file") flags.Bool("discard-response-bodies", false, "Read but don't process or save HTTP response bodies") + flags.String("dns", types.DefaultDNSConfig().String(), "DNS resolver configuration. Possible ttl values are: 'inf' "+ + "for a persistent cache, '0' to disable the cache,\nor a positive duration, e.g. '1s', '1m', etc. "+ + "Milliseconds are assumed if no unit is provided.\n"+ + "Possible select values to return a single IP are: 'first', 'random' or 'roundRobin'.\n"+ + "Possible policy values are: 'preferIPv4', 'preferIPv6', 'onlyIPv4', 'onlyIPv6' or 'any'.\n") return flags } @@ -248,6 +253,14 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) { opts.ConsoleOutput = null.StringFrom(redirectConFile) } + if dns, err := flags.GetString("dns"); err != nil { + return opts, err + } else if dns != "" { + if err := opts.DNS.UnmarshalText([]byte(dns)); err != nil { + return opts, err + } + } + return opts, nil } diff --git a/core/local/local_test.go b/core/local/local_test.go index c7ace16d503..7b958df9028 100644 --- a/core/local/local_test.go +++ b/core/local/local_test.go @@ -24,6 +24,7 @@ import ( "context" "errors" "fmt" + "io/ioutil" "net" "net/url" "reflect" @@ -47,6 +48,7 @@ import ( "github.com/loadimpact/k6/lib/testutils" "github.com/loadimpact/k6/lib/testutils/httpmultibin" "github.com/loadimpact/k6/lib/testutils/minirunner" + "github.com/loadimpact/k6/lib/testutils/mockresolver" "github.com/loadimpact/k6/lib/types" "github.com/loadimpact/k6/loader" "github.com/loadimpact/k6/stats" @@ -974,6 +976,106 @@ func TestExecutionSchedulerIsRunning(t *testing.T) { assert.NoError(t, <-err) } +// TestDNSResolver checks the DNS resolution behavior at the ExecutionScheduler level. +func TestDNSResolver(t *testing.T) { + tb := httpmultibin.NewHTTPMultiBin(t) + defer tb.Cleanup() + sr := tb.Replacer.Replace + script := sr(` + import http from "k6/http"; + import { sleep } from "k6"; + + export let options = { + vus: 1, + iterations: 8, + noConnectionReuse: true, + } + + export default function () { + const res = http.get("http://myhost:HTTPBIN_PORT/", { timeout: 50 }); + sleep(0.7); // somewhat uneven multiple of 0.5 to minimize races with asserts + }`) + + t.Run("cache", func(t *testing.T) { + testCases := map[string]struct { + opts lib.Options + expLogEntries int + }{ + "default": { // IPs are cached for 5m + lib.Options{DNS: types.DefaultDNSConfig()}, 0, + }, + "0": { // cache is disabled, every request does a DNS lookup + lib.Options{DNS: types.DNSConfig{ + TTL: null.StringFrom("0"), + Select: types.NullDNSSelect{DNSSelect: types.DNSfirst, Valid: true}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false}, + }}, 5, + }, + "1000": { // cache IPs for 1s, check that unitless values are interpreted as ms + lib.Options{DNS: types.DNSConfig{ + TTL: null.StringFrom("1000"), + Select: types.NullDNSSelect{DNSSelect: types.DNSfirst, Valid: true}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false}, + }}, 4, + }, + "3s": { + lib.Options{DNS: types.DNSConfig{ + TTL: null.StringFrom("3s"), + Select: types.NullDNSSelect{DNSSelect: types.DNSfirst, Valid: true}, + Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false}, + }}, 3, + }, + } + + expErr := sr(`dial tcp 127.0.0.254:HTTPBIN_PORT: connect: connection refused`) + if runtime.GOOS == "windows" { + expErr = "context deadline exceeded" + } + for name, tc := range testCases { + tc := tc + t.Run(name, func(t *testing.T) { + logger := logrus.New() + logger.SetOutput(ioutil.Discard) + logHook := testutils.SimpleLogrusHook{HookedLevels: []logrus.Level{logrus.WarnLevel}} + logger.AddHook(&logHook) + + runner, err := js.New(logger, &loader.SourceData{ + URL: &url.URL{Path: "/script.js"}, Data: []byte(script), + }, nil, lib.RuntimeOptions{}) + require.NoError(t, err) + + mr := mockresolver.New(nil, net.LookupIP) + runner.ActualResolver = mr.LookupIPAll + + ctx, cancel, execScheduler, samples := newTestExecutionScheduler(t, runner, logger, tc.opts) + defer cancel() + + mr.Set("myhost", sr("HTTPBIN_IP")) + time.AfterFunc(1700*time.Millisecond, func() { + mr.Set("myhost", "127.0.0.254") + }) + defer mr.Unset("myhost") + + errCh := make(chan error, 1) + go func() { errCh <- execScheduler.Run(ctx, ctx, samples) }() + + select { + case err := <-errCh: + require.NoError(t, err) + entries := logHook.Drain() + require.Len(t, entries, tc.expLogEntries) + for _, entry := range entries { + require.IsType(t, &url.Error{}, entry.Data["error"]) + assert.EqualError(t, entry.Data["error"].(*url.Error).Err, expErr) + } + case <-time.After(10 * time.Second): + t.Fatal("timed out") + } + }) + } + }) +} + func TestRealTimeAndSetupTeardownMetrics(t *testing.T) { if runtime.GOOS == "windows" { t.Skip() @@ -1100,7 +1202,10 @@ func TestRealTimeAndSetupTeardownMetrics(t *testing.T) { getDummyTrail := func(group string, emitIterations bool, addExpTags ...string) stats.SampleContainer { expTags := []string{"group", group} expTags = append(expTags, addExpTags...) - return netext.NewDialer(net.Dialer{}).GetTrail(time.Now(), time.Now(), + return netext.NewDialer( + net.Dialer{}, + netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4), + ).GetTrail(time.Now(), time.Now(), true, emitIterations, getTags(expTags...)) } diff --git a/go.mod b/go.mod index ae2f6a78dc4..50a614da78b 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c github.com/andybalholm/cascadia v1.0.0 // indirect github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect - github.com/dop251/goja v0.0.0-20201007100345-a8e472c705eb + github.com/dlclark/regexp2 v1.4.1-0.20201013204808-346446b5b182 // indirect + github.com/dop251/goja v0.0.0-20201008094107-f97e50db25ec github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 github.com/eapache/go-resiliency v1.1.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20160609142408-bb955e01b934 // indirect @@ -68,7 +68,6 @@ require ( github.com/urfave/negroni v0.3.1-0.20180130044549-22c5532ea862 github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect - github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 diff --git a/go.sum b/go.sum index ae77725981f..9eb780c8a04 100644 --- a/go.sum +++ b/go.sum @@ -25,10 +25,10 @@ github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb h1:tUf55Po0vzOendQ github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb/go.mod h1:U0vRfAucUOohvdCxt5MWLF+TePIL0xbCkbKIiV8TQCE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dop251/goja v0.0.0-20201007100345-a8e472c705eb h1:358PKppXCE79d1HvEQWYtc5Moihb1pURjNhPZkouSPM= -github.com/dop251/goja v0.0.0-20201007100345-a8e472c705eb/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= +github.com/dlclark/regexp2 v1.4.1-0.20201013204808-346446b5b182 h1:P/IEt6bRdU/7F+i9VvzOgtLVg9MWMe4UODEllD/nH3o= +github.com/dlclark/regexp2 v1.4.1-0.20201013204808-346446b5b182/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dop251/goja v0.0.0-20201008094107-f97e50db25ec h1:I7aPaxeou6Da490jBeG2DuOa6mgj6N+uXi0AvidUMJc= +github.com/dop251/goja v0.0.0-20201008094107-f97e50db25ec/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= @@ -187,8 +187,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= -github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 h1:EVObHAr8DqpoJCVv6KYTle8FEImKhtkfcZetNqxDoJQ= -github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5 h1:Zs6mpwXvlqpF9zHl5XaN0p5V4J9XvP+WBuiuXyIgqvc= github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5/go.mod h1:c1r+Ob9tUTPB0FKWO1+x+Hsc/zNa45WdGq7Y38Ybip0= @@ -197,24 +195,19 @@ golang.org/x/crypto v0.0.0-20180308185624-c7dcf104e3a7/go.mod h1:6SG95UA2DQfeDnf golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -231,20 +224,16 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375 h1:SjQ2+AKWgZLc1xej6WSzL+Dfs5Uyd5xcZH1mGC411IA= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0 h1:ZvI3lsq5AIkr7axxmT3tfwFlJVRFLqe6Fp0W03+MJ38= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200903010400-9bfcb5116336 h1:ZcAny/XH59BbzUOKydQpvIlklwibW3T9SvDE5cGhdzc= @@ -266,12 +255,10 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= diff --git a/js/bundle.go b/js/bundle.go index e0fcb5ce33c..a179dd107e0 100644 --- a/js/bundle.go +++ b/js/bundle.go @@ -249,7 +249,7 @@ func (b *Bundle) Instantiate(logger logrus.FieldLogger, vuID int64) (bi *BundleI } // Grab any exported functions that could be executed. These were - // already pre-validated in NewBundle(), just get them here. + // already pre-validated in cmd.validateScenarioConfig(), just get them here. exports := rt.Get("exports").ToObject(rt) for k := range b.exports { fn, _ := goja.AssertFunction(exports.Get(k)) diff --git a/js/initcontext_test.go b/js/initcontext_test.go index e6d73c041eb..768aee5e960 100644 --- a/js/initcontext_test.go +++ b/js/initcontext_test.go @@ -43,6 +43,7 @@ import ( "github.com/loadimpact/k6/lib/consts" "github.com/loadimpact/k6/lib/netext" "github.com/loadimpact/k6/lib/testutils" + "github.com/loadimpact/k6/lib/types" "github.com/loadimpact/k6/stats" ) @@ -388,11 +389,14 @@ func TestRequestWithBinaryFile(t *testing.T) { Logger: logger, Group: root, Transport: &http.Transport{ - DialContext: (netext.NewDialer(net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 60 * time.Second, - DualStack: true, - })).DialContext, + DialContext: (netext.NewDialer( + net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 60 * time.Second, + DualStack: true, + }, + netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4), + )).DialContext, }, BPool: bpool.NewBufferPool(1), Samples: make(chan stats.SampleContainer, 500), diff --git a/js/runner.go b/js/runner.go index eb584e64bed..370ebf17790 100644 --- a/js/runner.go +++ b/js/runner.go @@ -36,7 +36,6 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/afero" - "github.com/viki-org/dnscache" "golang.org/x/net/http2" "golang.org/x/time/rate" @@ -44,6 +43,7 @@ import ( "github.com/loadimpact/k6/lib" "github.com/loadimpact/k6/lib/consts" "github.com/loadimpact/k6/lib/netext" + "github.com/loadimpact/k6/lib/types" "github.com/loadimpact/k6/loader" "github.com/loadimpact/k6/stats" ) @@ -60,8 +60,10 @@ type Runner struct { defaultGroup *lib.Group BaseDialer net.Dialer - Resolver *dnscache.Resolver - RPSLimit *rate.Limiter + Resolver netext.Resolver + // TODO: Remove ActualResolver, it's a hack to simplify mocking in tests. + ActualResolver netext.MultiResolver + RPSLimit *rate.Limiter console *console setupData []byte @@ -95,6 +97,7 @@ func newFromBundle(logger *logrus.Logger, b *Bundle) (*Runner, error) { return nil, err } + defDNS := types.DefaultDNSConfig() r := &Runner{ Bundle: b, Logger: logger, @@ -104,8 +107,10 @@ func newFromBundle(logger *logrus.Logger, b *Bundle) (*Runner, error) { KeepAlive: 30 * time.Second, DualStack: true, }, - console: newConsole(logger), - Resolver: dnscache.New(0), + console: newConsole(logger), + Resolver: netext.NewResolver( + net.LookupIP, 0, defDNS.Select.DNSSelect, defDNS.Policy.DNSPolicy), + ActualResolver: net.LookupIP, } err = r.SetOptions(r.Bundle.Options) @@ -319,9 +324,64 @@ func (r *Runner) SetOptions(opts lib.Options) error { r.console = c } + // FIXME: Resolver probably shouldn't be reset here... + // It's done because the js.Runner is created before the full + // configuration has been processed, at which point we don't have + // access to the DNSConfig, and need to wait for this SetOptions + // call that happens after all config has been assembled. + // We could make DNSConfig part of RuntimeOptions, but that seems + // conceptually wrong since the JS runtime doesn't care about it + // (it needs the actual resolver, not the config), and it would + // require an additional field on Bundle to pass the config through, + // which is arguably worse than this. + if err := r.setResolver(opts.DNS); err != nil { + return err + } + return nil } +func (r *Runner) setResolver(dns types.DNSConfig) error { + ttl, err := parseTTL(dns.TTL.String) + if err != nil { + return err + } + + dnsSel := dns.Select + if !dnsSel.Valid { + dnsSel = types.DefaultDNSConfig().Select + } + dnsPol := dns.Policy + if !dnsPol.Valid { + dnsPol = types.DefaultDNSConfig().Policy + } + r.Resolver = netext.NewResolver( + r.ActualResolver, ttl, dnsSel.DNSSelect, dnsPol.DNSPolicy) + + return nil +} + +func parseTTL(ttlS string) (time.Duration, error) { + ttl := time.Duration(0) + switch ttlS { + case "inf": + // cache "infinitely" + ttl = time.Hour * 24 * 365 + case "0": + // disable cache + case "": + ttlS = types.DefaultDNSConfig().TTL.String + fallthrough + default: + var err error + ttl, err = types.ParseExtendedDurationMs(ttlS) + if ttl < 0 || err != nil { + return ttl, fmt.Errorf("invalid DNS TTL: %s", ttlS) + } + } + return ttl, nil +} + // Runs an exported function in its own temporary VU, optionally with an argument. Execution is // interrupted if the context expires. No error is returned if the part does not exist. func (r *Runner) runPart(ctx context.Context, out chan<- stats.SampleContainer, name string, arg interface{}) (goja.Value, error) { diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index 050c4660067..9e940e008fb 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -28,27 +28,18 @@ import ( "sync/atomic" "time" - "github.com/pkg/errors" - "github.com/viki-org/dnscache" - "github.com/loadimpact/k6/lib" "github.com/loadimpact/k6/lib/metrics" "github.com/loadimpact/k6/lib/types" "github.com/loadimpact/k6/stats" ) -// dnsResolver is an interface that fetches dns information -// about a given address. -type dnsResolver interface { - FetchOne(address string) (net.IP, error) -} - // Dialer wraps net.Dialer and provides k6 specific functionality - // tracing, blacklists and DNS cache and aliases. type Dialer struct { net.Dialer - Resolver dnsResolver + Resolver Resolver Blacklist []*lib.IPNet BlockedHostnames *types.HostnameTrie Hosts map[string]*lib.HostAddress @@ -57,12 +48,8 @@ type Dialer struct { BytesWritten int64 } -// NewDialer constructs a new Dialer and initializes its cache. -func NewDialer(dialer net.Dialer) *Dialer { - return newDialerWithResolver(dialer, dnscache.New(0)) -} - -func newDialerWithResolver(dialer net.Dialer, resolver dnsResolver) *Dialer { +// NewDialer constructs a new Dialer with the given DNS resolver. +func NewDialer(dialer net.Dialer, resolver Resolver) *Dialer { return &Dialer{ Dialer: dialer, Resolver: resolver, @@ -191,17 +178,13 @@ func (d *Dialer) findRemote(addr string) (*lib.HostAddress, error) { return lib.NewHostAddress(ip, port) } - return d.fetchRemoteFromResolver(host, port) -} - -func (d *Dialer) fetchRemoteFromResolver(host, port string) (*lib.HostAddress, error) { - ip, err := d.Resolver.FetchOne(host) + ip, err = d.Resolver.LookupIP(host) if err != nil { return nil, err } if ip == nil { - return nil, errors.Errorf("lookup %s: no such host", host) + return nil, fmt.Errorf("lookup %s: no such host", host) } return lib.NewHostAddress(ip, port) diff --git a/lib/netext/dialer_test.go b/lib/netext/dialer_test.go index 71db79cc07e..46125005f3c 100644 --- a/lib/netext/dialer_test.go +++ b/lib/netext/dialer_test.go @@ -24,19 +24,15 @@ import ( "net" "testing" + "github.com/stretchr/testify/require" + "github.com/loadimpact/k6/lib" + "github.com/loadimpact/k6/lib/testutils/mockresolver" "github.com/loadimpact/k6/lib/types" - "github.com/stretchr/testify/require" ) -type testResolver struct { - hosts map[string]net.IP -} - -func (r testResolver) FetchOne(host string) (net.IP, error) { return r.hosts[host], nil } - func TestDialerAddr(t *testing.T) { - dialer := newDialerWithResolver(net.Dialer{}, newResolver()) + dialer := NewDialer(net.Dialer{}, newResolver()) dialer.Hosts = map[string]*lib.HostAddress{ "example.com": {IP: net.ParseIP("3.4.5.6")}, "example.com:443": {IP: net.ParseIP("3.4.5.6"), Port: 8443}, @@ -95,7 +91,7 @@ func TestDialerAddr(t *testing.T) { } func TestDialerAddrBlockHostnamesStar(t *testing.T) { - dialer := newDialerWithResolver(net.Dialer{}, newResolver()) + dialer := NewDialer(net.Dialer{}, newResolver()) dialer.Hosts = map[string]*lib.HostAddress{ "example.com": {IP: net.ParseIP("3.4.5.6")}, } @@ -129,12 +125,12 @@ func TestDialerAddrBlockHostnamesStar(t *testing.T) { } } -func newResolver() testResolver { - return testResolver{ - hosts: map[string]net.IP{ - "example-resolver.com": net.ParseIP("1.2.3.4"), - "example-deny-resolver.com": net.ParseIP("8.9.10.11"), - "example-ipv6-deny-resolver.com": net.ParseIP("::1"), - }, - } +func newResolver() *mockresolver.MockResolver { + return mockresolver.New( + map[string][]net.IP{ + "example-resolver.com": {net.ParseIP("1.2.3.4")}, + "example-deny-resolver.com": {net.ParseIP("8.9.10.11")}, + "example-ipv6-deny-resolver.com": {net.ParseIP("::1")}, + }, nil, + ) } diff --git a/lib/netext/httpext/tracer_test.go b/lib/netext/httpext/tracer_test.go index c2b56974180..fb97b8ea3c0 100644 --- a/lib/netext/httpext/tracer_test.go +++ b/lib/netext/httpext/tracer_test.go @@ -42,6 +42,7 @@ import ( "github.com/loadimpact/k6/lib/metrics" "github.com/loadimpact/k6/lib/netext" + "github.com/loadimpact/k6/lib/types" "github.com/loadimpact/k6/stats" ) @@ -55,7 +56,10 @@ func TestTracer(t *testing.T) { transport, ok := srv.Client().Transport.(*http.Transport) assert.True(t, ok) - transport.DialContext = netext.NewDialer(net.Dialer{}).DialContext + transport.DialContext = netext.NewDialer( + net.Dialer{}, + netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4), + ).DialContext var prev int64 assertLaterOrZero := func(t *testing.T, val int64, canBeZero bool) { diff --git a/lib/netext/resolver.go b/lib/netext/resolver.go new file mode 100644 index 00000000000..25795a2ef40 --- /dev/null +++ b/lib/netext/resolver.go @@ -0,0 +1,190 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package netext + +import ( + "math/rand" + "net" + "sync" + "time" + + "github.com/loadimpact/k6/lib/types" +) + +// MultiResolver returns all IP addresses for the given host. +type MultiResolver func(host string) ([]net.IP, error) + +// Resolver is an interface that returns DNS information about a given host. +type Resolver interface { + LookupIP(host string) (net.IP, error) +} + +type resolver struct { + resolve MultiResolver + selectIndex types.DNSSelect + policy types.DNSPolicy + rrm *sync.Mutex + rand *rand.Rand + roundRobin map[string]uint8 +} + +type cacheRecord struct { + ips []net.IP + lastLookup time.Time +} + +type cacheResolver struct { + resolver + ttl time.Duration + cm *sync.Mutex + cache map[string]cacheRecord +} + +// NewResolver returns a new DNS resolver. If ttl is not 0, responses +// will be cached per host for the specified period. The IP returned from +// LookupIP() will be selected based on the given sel and pol values. +func NewResolver( + actRes MultiResolver, ttl time.Duration, sel types.DNSSelect, pol types.DNSPolicy, +) Resolver { + r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec + res := resolver{ + resolve: actRes, + selectIndex: sel, + policy: pol, + rrm: &sync.Mutex{}, + rand: r, + roundRobin: make(map[string]uint8), + } + if ttl == 0 { + return &res + } + return &cacheResolver{ + resolver: res, + ttl: ttl, + cm: &sync.Mutex{}, + cache: make(map[string]cacheRecord), + } +} + +// LookupIP returns a single IP resolved for host, selected according to the +// configured select and policy options. +func (r *resolver) LookupIP(host string) (net.IP, error) { + ips, err := r.resolve(host) + if err != nil { + return nil, err + } + + ips = r.applyPolicy(ips) + return r.selectOne(host, ips), nil +} + +// LookupIP returns a single IP resolved for host, selected according to the +// configured select and policy options. Results are cached per host and will be +// refreshed if the last lookup time exceeds the configured TTL (not the TTL +// returned in the DNS record). +func (r *cacheResolver) LookupIP(host string) (net.IP, error) { + r.cm.Lock() + + var ips []net.IP + // TODO: Invalidate? When? + if cr, ok := r.cache[host]; ok && time.Now().Before(cr.lastLookup.Add(r.ttl)) { + ips = cr.ips + } else { + r.cm.Unlock() // The lookup could take some time, so unlock momentarily. + var err error + ips, err = r.resolve(host) + if err != nil { + return nil, err + } + ips = r.applyPolicy(ips) + r.cm.Lock() + r.cache[host] = cacheRecord{ips: ips, lastLookup: time.Now()} + } + + r.cm.Unlock() + + return r.selectOne(host, ips), nil +} + +func (r *resolver) selectOne(host string, ips []net.IP) net.IP { + if len(ips) == 0 { + return nil + } + + var ip net.IP + switch r.selectIndex { + case types.DNSfirst: + return ips[0] + case types.DNSroundRobin: + r.rrm.Lock() + // NOTE: This index approach is not stable and might result in returning + // repeated or skipped IPs if the records change during a test run. + ip = ips[int(r.roundRobin[host])%len(ips)] + r.roundRobin[host]++ + r.rrm.Unlock() + case types.DNSrandom: + r.rrm.Lock() + ip = ips[r.rand.Intn(len(ips))] + r.rrm.Unlock() + } + + return ip +} + +func (r *resolver) applyPolicy(ips []net.IP) (retIPs []net.IP) { + if r.policy == types.DNSany { + return ips + } + ip4, ip6 := groupByVersion(ips) + switch r.policy { + case types.DNSpreferIPv4: + retIPs = ip4 + if len(retIPs) == 0 { + retIPs = ip6 + } + case types.DNSpreferIPv6: + retIPs = ip6 + if len(retIPs) == 0 { + retIPs = ip4 + } + case types.DNSonlyIPv4: + retIPs = ip4 + case types.DNSonlyIPv6: + retIPs = ip6 + // Already checked above, but added to satisfy 'exhaustive' linter. + case types.DNSany: + retIPs = ips + } + + return +} + +func groupByVersion(ips []net.IP) (ip4 []net.IP, ip6 []net.IP) { + for _, ip := range ips { + if ip.To4() != nil { + ip4 = append(ip4, ip) + } else { + ip6 = append(ip6, ip) + } + } + + return +} diff --git a/lib/netext/resolver_test.go b/lib/netext/resolver_test.go new file mode 100644 index 00000000000..4ec75c2f87c --- /dev/null +++ b/lib/netext/resolver_test.go @@ -0,0 +1,116 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package netext + +import ( + "fmt" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/loadimpact/k6/lib/testutils/mockresolver" + "github.com/loadimpact/k6/lib/types" +) + +func TestResolver(t *testing.T) { + t.Parallel() + + host := "myhost" + mr := mockresolver.New(map[string][]net.IP{ + host: { + net.ParseIP("127.0.0.10"), + net.ParseIP("127.0.0.11"), + net.ParseIP("127.0.0.12"), + net.ParseIP("2001:db8::10"), + net.ParseIP("2001:db8::11"), + net.ParseIP("2001:db8::12"), + }, + }, nil) + + t.Run("LookupIP", func(t *testing.T) { + testCases := []struct { + ttl time.Duration + sel types.DNSSelect + pol types.DNSPolicy + expIP []net.IP + }{ + { + 0, types.DNSfirst, types.DNSpreferIPv4, + []net.IP{net.ParseIP("127.0.0.10")}, + }, + { + time.Second, types.DNSfirst, types.DNSpreferIPv4, + []net.IP{net.ParseIP("127.0.0.10")}, + }, + {0, types.DNSroundRobin, types.DNSonlyIPv6, []net.IP{ + net.ParseIP("2001:db8::10"), + net.ParseIP("2001:db8::11"), + net.ParseIP("2001:db8::12"), + net.ParseIP("2001:db8::10"), + }}, + { + 0, types.DNSfirst, types.DNSpreferIPv6, + []net.IP{net.ParseIP("2001:db8::10")}, + }, + {0, types.DNSroundRobin, types.DNSpreferIPv4, []net.IP{ + net.ParseIP("127.0.0.10"), + net.ParseIP("127.0.0.11"), + net.ParseIP("127.0.0.12"), + net.ParseIP("127.0.0.10"), + }}, + } + + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("%s_%s_%s", tc.ttl, tc.sel, tc.pol), func(t *testing.T) { + r := NewResolver(mr.LookupIPAll, tc.ttl, tc.sel, tc.pol) + ip, err := r.LookupIP(host) + require.NoError(t, err) + assert.Equal(t, tc.expIP[0], ip) + + if tc.ttl > 0 { + require.IsType(t, &cacheResolver{}, r) + cr := r.(*cacheResolver) + assert.Len(t, cr.cache, 1) + assert.Equal(t, tc.ttl, cr.ttl) + firstLookup := cr.cache[host].lastLookup + time.Sleep(cr.ttl + 100*time.Millisecond) + _, err = r.LookupIP(host) + require.NoError(t, err) + assert.True(t, cr.cache[host].lastLookup.After(firstLookup)) + } + + if tc.sel == types.DNSroundRobin { + ips := []net.IP{ip} + for i := 0; i < 3; i++ { + ip, err = r.LookupIP(host) + require.NoError(t, err) + ips = append(ips, ip) + } + assert.Equal(t, tc.expIP, ips) + } + }) + } + }) +} diff --git a/lib/options.go b/lib/options.go index 20555d4fd41..6dfec6ed4c7 100644 --- a/lib/options.go +++ b/lib/options.go @@ -307,6 +307,9 @@ type Options struct { // Limit HTTP requests per second. RPS null.Int `json:"rps" envconfig:"K6_RPS"` + // DNS handling configuration. + DNS types.DNSConfig `json:"dns" envconfig:"K6_DNS"` + // How many HTTP redirects do we follow? MaxRedirects null.Int `json:"maxRedirects" envconfig:"K6_MAX_REDIRECTS"` @@ -539,6 +542,15 @@ func (o Options) Apply(opts Options) Options { if opts.ConsoleOutput.Valid { o.ConsoleOutput = opts.ConsoleOutput } + if opts.DNS.TTL.Valid { + o.DNS.TTL = opts.DNS.TTL + } + if opts.DNS.Select.Valid { + o.DNS.Select = opts.DNS.Select + } + if opts.DNS.Policy.Valid { + o.DNS.Policy = opts.DNS.Policy + } return o } diff --git a/lib/testutils/httpmultibin/httpmultibin.go b/lib/testutils/httpmultibin/httpmultibin.go index 068616c1a09..2f85cb5b53f 100644 --- a/lib/testutils/httpmultibin/httpmultibin.go +++ b/lib/testutils/httpmultibin/httpmultibin.go @@ -53,6 +53,7 @@ import ( "github.com/loadimpact/k6/lib" "github.com/loadimpact/k6/lib/netext" "github.com/loadimpact/k6/lib/netext/httpext" + "github.com/loadimpact/k6/lib/types" ) // GetTLSClientConfig returns a TLS config that trusts the supplied @@ -316,8 +317,7 @@ func NewHTTPMultiBin(t testing.TB) *HTTPMultiBin { Timeout: 2 * time.Second, KeepAlive: 10 * time.Second, DualStack: true, - }) - + }, netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4)) dialer.Hosts = map[string]*lib.HostAddress{ httpDomain: httpDomainValue, httpsDomain: httpsDomainValue, diff --git a/lib/testutils/mockresolver/resolver.go b/lib/testutils/mockresolver/resolver.go new file mode 100644 index 00000000000..5e68d260893 --- /dev/null +++ b/lib/testutils/mockresolver/resolver.go @@ -0,0 +1,81 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package mockresolver + +import ( + "fmt" + "net" + "sync" +) + +// MockResolver implements netext.Resolver, and allows changing the host +// mapping at runtime. +type MockResolver struct { + m sync.RWMutex + hosts map[string][]net.IP + fallback func(host string) ([]net.IP, error) +} + +// New returns a new MockResolver. +func New(hosts map[string][]net.IP, fallback func(host string) ([]net.IP, error)) *MockResolver { + if hosts == nil { + hosts = make(map[string][]net.IP) + } + return &MockResolver{hosts: hosts, fallback: fallback} +} + +// LookupIP returns the first IP mapped for host. +func (r *MockResolver) LookupIP(host string) (net.IP, error) { + if ips, err := r.LookupIPAll(host); err != nil { + return nil, err + } else if len(ips) > 0 { + return ips[0], nil + } + return nil, nil +} + +// LookupIPAll returns all IPs mapped for host. It mimics the net.LookupIP +// signature so that it can be used to mock netext.LookupIP in tests. +func (r *MockResolver) LookupIPAll(host string) ([]net.IP, error) { + r.m.RLock() + defer r.m.RUnlock() + if ips, ok := r.hosts[host]; ok { + return ips, nil + } + if r.fallback != nil { + return r.fallback(host) + } + return nil, fmt.Errorf("lookup %s: no such host", host) +} + +// Set the host to resolve to ip. +func (r *MockResolver) Set(host, ip string) { + r.m.Lock() + defer r.m.Unlock() + r.hosts[host] = []net.IP{net.ParseIP(ip)} +} + +// Unset removes the host. +func (r *MockResolver) Unset(host string) { + r.m.Lock() + defer r.m.Unlock() + delete(r.hosts, host) +} diff --git a/lib/types/dns.go b/lib/types/dns.go new file mode 100644 index 00000000000..adcca440c5f --- /dev/null +++ b/lib/types/dns.go @@ -0,0 +1,248 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package types + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/kubernetes/helm/pkg/strvals" + "gopkg.in/guregu/null.v3" +) + +// DNSConfig is the DNS resolver configuration. +type DNSConfig struct { + // If positive, defines how long DNS lookups should be returned from the cache. + TTL null.String `json:"ttl"` + // Select specifies the strategy to use when picking a single IP if more than one is returned for a host name. + Select NullDNSSelect `json:"select"` + // Policy specifies how to handle returning of IPv4 or IPv6 addresses. + Policy NullDNSPolicy `json:"policy"` + // FIXME: Valid is unused and is only added to satisfy some logic in + // lib.Options.ForEachSpecified(), otherwise it would panic with + // `reflect: call of reflect.Value.Bool on zero Value`. + Valid bool `json:"-"` +} + +// DefaultDNSConfig returns the default DNS configuration. +func DefaultDNSConfig() DNSConfig { + return DNSConfig{ + TTL: null.NewString("5m", false), + Select: NullDNSSelect{DNSrandom, false}, + Policy: NullDNSPolicy{DNSpreferIPv4, false}, + } +} + +// DNSPolicy specifies the preference for handling IP versions in DNS resolutions. +//go:generate enumer -type=DNSPolicy -trimprefix DNS -output dns_policy_gen.go +type DNSPolicy uint8 + +// These are lower camel cased since enumer doesn't support it as a transform option. +// See https://github.com/alvaroloes/enumer/pull/60 . +const ( + // DNSpreferIPv4 returns an IPv4 address if available, falling back to IPv6 otherwise. + DNSpreferIPv4 DNSPolicy = iota + 1 + // DNSpreferIPv6 returns an IPv6 address if available, falling back to IPv4 otherwise. + DNSpreferIPv6 + // DNSonlyIPv4 only returns an IPv4 address and the resolution will fail if no IPv4 address is found. + DNSonlyIPv4 + // DNSonlyIPv6 only returns an IPv6 address and the resolution will fail if no IPv6 address is found. + DNSonlyIPv6 + // DNSany returns any resolved address regardless of version. + DNSany +) + +// UnmarshalJSON converts JSON data to a valid DNSPolicy +func (d *DNSPolicy) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte(`null`)) { + return nil + } + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + v, err := DNSPolicyString(s) + if err != nil { + return err + } + *d = v + return nil +} + +// MarshalJSON returns the JSON representation of d. +func (d DNSPolicy) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// NullDNSPolicy is a nullable wrapper around DNSPolicy, required for the +// current configuration system. +type NullDNSPolicy struct { + DNSPolicy + Valid bool +} + +// UnmarshalJSON converts JSON data to a valid NullDNSPolicy. +func (d *NullDNSPolicy) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte(`null`)) { + return nil + } + if err := json.Unmarshal(data, &d.DNSPolicy); err != nil { + return err + } + d.Valid = true + return nil +} + +// MarshalJSON returns the JSON representation of d. +func (d NullDNSPolicy) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte(`null`), nil + } + return json.Marshal(d.DNSPolicy) +} + +// DNSSelect is the strategy to use when picking a single IP if more than one +// is returned for a host name. +//go:generate enumer -type=DNSSelect -trimprefix DNS -output dns_select_gen.go +type DNSSelect uint8 + +// These are lower camel cased since enumer doesn't support it as a transform option. +// See https://github.com/alvaroloes/enumer/pull/60 . +const ( + // DNSfirst returns the first IP from the response. + DNSfirst DNSSelect = iota + 1 + // DNSroundRobin rotates the IP returned on each lookup. + DNSroundRobin + // DNSrandom returns a random IP from the response. + DNSrandom +) + +// UnmarshalJSON converts JSON data to a valid DNSSelect +func (d *DNSSelect) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte(`null`)) { + return nil + } + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + v, err := DNSSelectString(s) + if err != nil { + return err + } + *d = v + return nil +} + +// MarshalJSON returns the JSON representation of d. +func (d DNSSelect) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// NullDNSSelect is a nullable wrapper around DNSSelect, required for the +// current configuration system. +type NullDNSSelect struct { + DNSSelect + Valid bool +} + +// UnmarshalJSON converts JSON data to a valid NullDNSSelect. +func (d *NullDNSSelect) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte(`null`)) { + return nil + } + if err := json.Unmarshal(data, &d.DNSSelect); err != nil { + return err + } + d.Valid = true + return nil +} + +// MarshalJSON returns the JSON representation of d. +func (d NullDNSSelect) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte(`null`), nil + } + return json.Marshal(d.DNSSelect) +} + +// String implements fmt.Stringer. +func (c DNSConfig) String() string { + return fmt.Sprintf("ttl=%s,select=%s,policy=%s", + c.TTL.String, c.Select.String(), c.Policy.String()) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (c *DNSConfig) UnmarshalJSON(data []byte) error { + var s struct { + TTL null.String `json:"ttl"` + Select NullDNSSelect `json:"select"` + Policy NullDNSPolicy `json:"policy"` + } + if err := json.Unmarshal(data, &s); err != nil { + return err + } + c.TTL = s.TTL + c.Select = s.Select + c.Policy = s.Policy + return nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (c *DNSConfig) UnmarshalText(text []byte) error { + if string(text) == DefaultDNSConfig().String() { + *c = DefaultDNSConfig() + return nil + } + params, err := strvals.Parse(string(text)) + if err != nil { + return err + } + return c.unmarshal(params) +} + +func (c *DNSConfig) unmarshal(params map[string]interface{}) error { + for k, v := range params { + switch k { + case "policy": + p, err := DNSPolicyString(v.(string)) + if err != nil { + return err + } + c.Policy.DNSPolicy = p + c.Policy.Valid = true + case "select": + s, err := DNSSelectString(v.(string)) + if err != nil { + return err + } + c.Select.DNSSelect = s + c.Select.Valid = true + case "ttl": + ttlv := fmt.Sprintf("%v", v) + c.TTL = null.StringFrom(ttlv) + default: + return fmt.Errorf("unknown DNS configuration field: %s", k) + } + } + return nil +} diff --git a/lib/types/dns_policy_gen.go b/lib/types/dns_policy_gen.go new file mode 100644 index 00000000000..85feb6d24a7 --- /dev/null +++ b/lib/types/dns_policy_gen.go @@ -0,0 +1,54 @@ +// Code generated by "enumer -type=DNSPolicy -trimprefix DNS -output dns_policy_gen.go"; DO NOT EDIT. + +// +package types + +import ( + "fmt" +) + +const _DNSPolicyName = "preferIPv4preferIPv6onlyIPv4onlyIPv6any" + +var _DNSPolicyIndex = [...]uint8{0, 10, 20, 28, 36, 39} + +func (i DNSPolicy) String() string { + i -= 1 + if i >= DNSPolicy(len(_DNSPolicyIndex)-1) { + return fmt.Sprintf("DNSPolicy(%d)", i+1) + } + return _DNSPolicyName[_DNSPolicyIndex[i]:_DNSPolicyIndex[i+1]] +} + +var _DNSPolicyValues = []DNSPolicy{1, 2, 3, 4, 5} + +var _DNSPolicyNameToValueMap = map[string]DNSPolicy{ + _DNSPolicyName[0:10]: 1, + _DNSPolicyName[10:20]: 2, + _DNSPolicyName[20:28]: 3, + _DNSPolicyName[28:36]: 4, + _DNSPolicyName[36:39]: 5, +} + +// DNSPolicyString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func DNSPolicyString(s string) (DNSPolicy, error) { + if val, ok := _DNSPolicyNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to DNSPolicy values", s) +} + +// DNSPolicyValues returns all values of the enum +func DNSPolicyValues() []DNSPolicy { + return _DNSPolicyValues +} + +// IsADNSPolicy returns "true" if the value is listed in the enum definition. "false" otherwise +func (i DNSPolicy) IsADNSPolicy() bool { + for _, v := range _DNSPolicyValues { + if i == v { + return true + } + } + return false +} diff --git a/lib/types/dns_select_gen.go b/lib/types/dns_select_gen.go new file mode 100644 index 00000000000..347aaec7a61 --- /dev/null +++ b/lib/types/dns_select_gen.go @@ -0,0 +1,52 @@ +// Code generated by "enumer -type=DNSSelect -trimprefix DNS -output dns_select_gen.go"; DO NOT EDIT. + +// +package types + +import ( + "fmt" +) + +const _DNSSelectName = "firstroundRobinrandom" + +var _DNSSelectIndex = [...]uint8{0, 5, 15, 21} + +func (i DNSSelect) String() string { + i -= 1 + if i >= DNSSelect(len(_DNSSelectIndex)-1) { + return fmt.Sprintf("DNSSelect(%d)", i+1) + } + return _DNSSelectName[_DNSSelectIndex[i]:_DNSSelectIndex[i+1]] +} + +var _DNSSelectValues = []DNSSelect{1, 2, 3} + +var _DNSSelectNameToValueMap = map[string]DNSSelect{ + _DNSSelectName[0:5]: 1, + _DNSSelectName[5:15]: 2, + _DNSSelectName[15:21]: 3, +} + +// DNSSelectString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func DNSSelectString(s string) (DNSSelect, error) { + if val, ok := _DNSSelectNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to DNSSelect values", s) +} + +// DNSSelectValues returns all values of the enum +func DNSSelectValues() []DNSSelect { + return _DNSSelectValues +} + +// IsADNSSelect returns "true" if the value is listed in the enum definition. "false" otherwise +func (i DNSSelect) IsADNSSelect() bool { + for _, v := range _DNSSelectValues { + if i == v { + return true + } + } + return false +} diff --git a/lib/types/types.go b/lib/types/types.go index e9ffeab037f..03609850e59 100644 --- a/lib/types/types.go +++ b/lib/types/types.go @@ -123,6 +123,16 @@ func ParseExtendedDuration(data string) (result time.Duration, err error) { return time.Duration(days)*24*time.Hour + hours, nil } +// ParseExtendedDurationMs wraps ParseExtendedDuration while assuming +// millisecond values if data is provided with no units. +// TODO: Merge this into ParseExtendedDuration once it's safe to do so globally. +func ParseExtendedDurationMs(data string) (result time.Duration, err error) { + if t, errp := strconv.ParseFloat(data, 32); errp == nil { + data = fmt.Sprintf("%.2fms", t) + } + return ParseExtendedDuration(data) +} + // UnmarshalText converts text data to Duration func (d *Duration) UnmarshalText(data []byte) error { v, err := ParseExtendedDuration(string(data)) diff --git a/vendor/github.com/dlclark/regexp2/README.md b/vendor/github.com/dlclark/regexp2/README.md index 4e4abb4c6f5..f92f8b10bee 100644 --- a/vendor/github.com/dlclark/regexp2/README.md +++ b/vendor/github.com/dlclark/regexp2/README.md @@ -39,6 +39,24 @@ Group 0 is embedded in the Match. Group 0 is an automatically-assigned group th The __last__ capture is embedded in each group, so `g.String()` will return the same thing as `g.Capture.String()` and `g.Captures[len(g.Captures)-1].String()`. +If you want to find multiple matches from a single input string you should use the `FindNextMatch` method. For example, to implement a function similar to `regexp.FindAllString`: + +```go +func regexp2FindAllString(re *regexp2.Regexp, s string) []string { + var matches []string + m, _ := re.FindStringMatch(s) + for m != nil { + matches = append(matches, m.String()) + m, _ = re.FindNextMatch(m) + } + return matches +} +``` + +`FindNextMatch` is optmized so that it re-uses the underlying string/rune slice. + +The internals of `regexp2` always operate on `[]rune` so `Index` and `Length` data in a `Match` always reference a position in `rune`s rather than `byte`s (even if the input was given as a string). This is a dramatic difference between `regexp` and `regexp2`. It's advisable to use the provided `String()` methods to avoid having to work with indices. + ## Compare `regexp` and `regexp2` | Category | regexp | regexp2 | | --- | --- | --- | diff --git a/vendor/github.com/dlclark/regexp2/syntax/prefix.go b/vendor/github.com/dlclark/regexp2/syntax/prefix.go index 011ef0b4168..f6716886293 100644 --- a/vendor/github.com/dlclark/regexp2/syntax/prefix.go +++ b/vendor/github.com/dlclark/regexp2/syntax/prefix.go @@ -712,7 +712,7 @@ func (b *BmPrefix) Scan(text []rune, index, beglimit, endlimit int) int { if chTest != b.pattern[match] { advance = b.positive[match] - if (chTest & 0xFF80) == 0 { + if chTest < 128 { test2 = (match - startmatch) + b.negativeASCII[chTest] } else if chTest < 0xffff && len(b.negativeUnicode) > 0 { unicodeLookup = b.negativeUnicode[chTest>>8] diff --git a/vendor/github.com/dop251/goja/regexp.go b/vendor/github.com/dop251/goja/regexp.go index e8fd7d17a19..2d020aa6821 100644 --- a/vendor/github.com/dop251/goja/regexp.go +++ b/vendor/github.com/dop251/goja/regexp.go @@ -467,7 +467,9 @@ func (r *regexpWrapper) findSubmatchIndexUnicode(s unicodeString, fullUnicode bo posMap, runes, _, _ := buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader(0)}, s.length(), 0) res := wrapped.FindReaderSubmatchIndex(&arrayRuneReader{runes: runes}) for i, item := range res { - res[i] = posMap[item] + if item >= 0 { + res[i] = posMap[item] + } } return res } diff --git a/vendor/github.com/viki-org/dnscache/dnscache.go b/vendor/github.com/viki-org/dnscache/dnscache.go deleted file mode 100644 index 74d6bd61fcb..00000000000 --- a/vendor/github.com/viki-org/dnscache/dnscache.go +++ /dev/null @@ -1,77 +0,0 @@ -package dnscache -// Package dnscache caches DNS lookups - -import ( - "net" - "sync" - "time" -) - -type Resolver struct { - lock sync.RWMutex - cache map[string][]net.IP -} - -func New(refreshRate time.Duration) *Resolver { - resolver := &Resolver { - cache: make(map[string][]net.IP, 64), - } - if refreshRate > 0 { - go resolver.autoRefresh(refreshRate) - } - return resolver -} - -func (r *Resolver) Fetch(address string) ([]net.IP, error) { - r.lock.RLock() - ips, exists := r.cache[address] - r.lock.RUnlock() - if exists { return ips, nil } - - return r.Lookup(address) -} - -func (r *Resolver) FetchOne(address string) (net.IP, error) { - ips, err := r.Fetch(address) - if err != nil || len(ips) == 0 { return nil, err} - return ips[0], nil -} - -func (r *Resolver) FetchOneString(address string) (string, error) { - ip, err := r.FetchOne(address) - if err != nil || ip == nil { return "", err } - return ip.String(), nil -} - -func (r *Resolver) Refresh() { - i := 0 - r.lock.RLock() - addresses := make([]string, len(r.cache)) - for key, _ := range r.cache { - addresses[i] = key - i++ - } - r.lock.RUnlock() - - for _, address := range addresses { - r.Lookup(address) - time.Sleep(time.Second * 2) - } -} - -func (r *Resolver) Lookup(address string) ([]net.IP, error) { - ips, err := net.LookupIP(address) - if err != nil { return nil, err } - - r.lock.Lock() - r.cache[address] = ips - r.lock.Unlock() - return ips, nil -} - -func (r *Resolver) autoRefresh(rate time.Duration) { - for { - time.Sleep(rate) - r.Refresh() - } -} diff --git a/vendor/github.com/viki-org/dnscache/license.txt b/vendor/github.com/viki-org/dnscache/license.txt deleted file mode 100644 index 8a7d969ed49..00000000000 --- a/vendor/github.com/viki-org/dnscache/license.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2013 Viki Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/viki-org/dnscache/readme.md b/vendor/github.com/viki-org/dnscache/readme.md deleted file mode 100644 index 8c737aac35d..00000000000 --- a/vendor/github.com/viki-org/dnscache/readme.md +++ /dev/null @@ -1,38 +0,0 @@ -### A DNS cache for Go -CGO is used to lookup domain names. Given enough concurrent requests and the slightest hiccup in name resolution, it's quite easy to end up with blocked/leaking goroutines. - -The issue is documented at - -The Go team's singleflight solution (which isn't in stable yet) is rather elegant. However, it only eliminates concurrent lookups (thundering herd problems). Many systems can live with slightly stale resolve names, which means we can cacne DNS lookups and refresh them in the background. - -### Installation -Install using the "go get" command: - - go get github.com/viki-org/dnscache - -### Usage -The cache is thread safe. Create a new instance by specifying how long each entry should be cached (in seconds). Items will be refreshed in the background. - - //refresh items every 5 minutes - resolver := dnscache.New(time.Minute * 5) - - //get an array of net.IP - ips, _ := resolver.Fetch("api.viki.io") - - //get the first net.IP - ip, _ := resolver.FetchOne("api.viki.io") - - //get the first net.IP as string - ip, _ := resolver.FetchOneString("api.viki.io") - -If you are using an `http.Transport`, you can use this cache by speficifying a -`Dial` function: - - transport := &http.Transport { - MaxIdleConnsPerHost: 64, - Dial: func(network string, address string) (net.Conn, error) { - separator := strings.LastIndex(address, ":") - ip, _ := dnscache.FetchString(address[:separator]) - return net.Dial("tcp", ip + address[separator:]) - }, - } diff --git a/vendor/modules.txt b/vendor/modules.txt index 229ecb738f2..5ec1181899f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -30,11 +30,11 @@ github.com/andybalholm/cascadia github.com/daaku/go.zipexe # github.com/davecgh/go-spew v1.1.1 => github.com/davecgh/go-spew v1.1.0 github.com/davecgh/go-spew/spew -# github.com/dlclark/regexp2 v1.4.0 +# github.com/dlclark/regexp2 v1.4.1-0.20201013204808-346446b5b182 ## explicit github.com/dlclark/regexp2 github.com/dlclark/regexp2/syntax -# github.com/dop251/goja v0.0.0-20201007100345-a8e472c705eb +# github.com/dop251/goja v0.0.0-20201008094107-f97e50db25ec ## explicit github.com/dop251/goja github.com/dop251/goja/ast @@ -245,9 +245,6 @@ github.com/valyala/bytebufferpool # github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 ## explicit github.com/valyala/fasttemplate -# github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 -## explicit -github.com/viki-org/dnscache # github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5 ## explicit github.com/zyedidia/highlight