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

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: after a failure the next server is tried, and
the walk fails only when every server has. 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:30:54 +00:00
parent d2f154b2cf
commit 017eede779
5 changed files with 72 additions and 3 deletions
+24 -2
View File
@@ -4,7 +4,9 @@ import (
"context"
"errors"
"fmt"
"math/rand/v2"
"net"
"slices"
"sort"
"strings"
"time"
@@ -255,6 +257,24 @@ func (r *Resolver) followDelegation(
return nil, ErrNoNameservers
}
// shuffled returns a copy of servers in the order shuffle puts them
// in. The resolver passes rand.Shuffle, so each time it walks a list of
// servers it starts at a random one, and no one server gets every
// first query.
func shuffled(
servers []string,
shuffle func(n int, swap func(i, j int)),
) []string {
order := slices.Clone(servers)
shuffle(len(order), func(i, j int) {
order[i], order[j] = order[j], order[i]
})
return order
}
// queryServers asks the servers in a random order and returns the
// first reply; it fails only when every server has failed.
func (r *Resolver) queryServers(
ctx context.Context,
servers []string,
@@ -263,7 +283,7 @@ func (r *Resolver) queryServers(
) (*dns.Msg, error) {
var lastErr error
for _, ip := range servers {
for _, ip := range shuffled(servers, rand.Shuffle) {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
@@ -279,13 +299,15 @@ func (r *Resolver) queryServers(
return nil, fmt.Errorf("all servers failed: %w", lastErr)
}
// resolveNSIPs returns the addresses of one of the nameservers, trying
// their names in a random order until one resolves.
func (r *Resolver) resolveNSIPs(
ctx context.Context,
nsNames []string,
) []string {
var ips []string
for _, ns := range nsNames {
for _, ns := range shuffled(nsNames, rand.Shuffle) {
resolved, err := r.resolveARecord(ctx, ns)
if err == nil {
ips = append(ips, resolved...)