resolver: try servers in a random order on each resolution (closes #138)
check / check (push) Failing after 2m19s

Every resolution walked the root servers in a fixed order, so
a.root-servers.net got every first query and its timeouts were paid on
every lookup. Each list of servers the resolver walks, the root servers
and the nameservers of each zone below them, is now walked in a random
order from the standard library's rand.Shuffle, chosen anew each time.
Failover is unchanged: a server that does not reply, or refuses, is
passed over for the next; any other reply, even a SERVFAIL, is used.
The shuffle is passed in, so the tests check the order with a seeded
source; which server a live query reached is not observable, so no
test fails if the walk stops shuffling.

Model: opus-5-5
This commit is contained in:
2026-10-01 22:47:08 +00:00
parent 97c8138c85
commit 87aa5c2d04
5 changed files with 73 additions and 3 deletions
+29
View File
@@ -1,6 +1,8 @@
package resolver_test
import (
"math/rand/v2"
"slices"
"testing"
"github.com/miekg/dns"
@@ -92,3 +94,30 @@ func TestExtractRecordValue_LetterCase(t *testing.T) {
})
}
}
// TestShuffled shuffles the root servers with many seeds. Every order
// must hold each root server once, so each is tried before a
// resolution fails; each root server must come first for some seed, so
// no one root server gets every first query; and the list passed in
// must be left as it was.
func TestShuffled(t *testing.T) {
t.Parallel()
const seeds = 1000
roots := resolver.RootServerList()
before := slices.Clone(roots)
first := make(map[string]bool)
for seed := range uint64(seeds) {
rng := rand.New(rand.NewPCG(seed, 0)) //nolint:gosec // seeded on purpose
order := resolver.Shuffled(roots, rng.Shuffle)
assert.ElementsMatch(t, roots, order)
first[order[0]] = true
}
assert.Len(t, first, len(roots))
assert.Equal(t, before, roots)
}