watcher tests: fewer queries per domain check, longer live attempts (closes #214)
check / check (push) Successful in 1m32s

ResolveIPAddresses, which a domain check runs for each nameserver, asked
every nameserver of the name's zone for all eight record types and read
only A, AAAA and CNAME. It now asks for those three, so a domain check
of example.com sends about a third fewer queries.

The per-attempt deadline in livednstest goes from 8 to 18 seconds. It
was sized for one lookup, while a watcher check sends over a hundred
queries in a row and on the CI runner ran past 8 seconds. Three attempts
still end under the 60-second cap.

A nameserver that answers none of the three queries now counts as not
answering even if it would have answered another type; the watcher keeps
the previous addresses either way.

Model: opus-5-5
This commit is contained in:
2026-10-02 02:07:00 +00:00
parent 82836b41fd
commit 30f8f05fd5
4 changed files with 55 additions and 21 deletions
+8 -5
View File
@@ -36,11 +36,14 @@ const (
// before the test fails.
attempts = 3
// AttemptTimeout bounds one attempt. Worst case for an operation
// is attempts * AttemptTimeout plus the backoff — about 26
// seconds, well inside the 90-second `go test -timeout` backstop
// even when several operations exhaust their attempts.
AttemptTimeout = 8 * time.Second
// AttemptTimeout bounds one attempt. It must fit the longest
// operation, a watcher check, which sends over a hundred queries one
// after another and on a slow build host takes several times as long
// as the few seconds it takes on a fast one. Worst case for an
// operation is attempts * AttemptTimeout plus the backoff — about 56
// seconds, under the suite's 60-second cap and inside the 90-second
// `go test -timeout` backstop.
AttemptTimeout = 18 * time.Second
// backoffBase is the delay after the first failed attempt; it is
// multiplied by backoffFactor each time.
+1 -1
View File
@@ -34,7 +34,7 @@ func (r *Resolver) QueryEachNS(
nameservers []string,
hostname string,
) (map[string]*NameserverResponse, error) {
return r.queryEachNS(ctx, nameservers, hostname)
return r.queryEachNS(ctx, nameservers, hostname, recordTypes())
}
// ResolveNSIPs exports resolveNSIPs for testing.
+44 -15
View File
@@ -533,12 +533,39 @@ func (r *Resolver) FindAuthoritativeNameservers(
return nil, ErrNoNameservers
}
// recordTypes returns the record types a nameserver is asked for when a
// name is checked.
func recordTypes() []uint16 {
return []uint16{
dns.TypeA, dns.TypeAAAA, dns.TypeCNAME,
dns.TypeMX, dns.TypeTXT, dns.TypeSRV,
dns.TypeCAA, dns.TypeNS,
}
}
// addressTypes returns the record types ResolveIPAddresses asks for,
// the only ones it reads.
func addressTypes() []uint16 {
return []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeCNAME}
}
// QueryNameserver queries a specific nameserver for all record
// types and builds a NameserverResponse.
func (r *Resolver) QueryNameserver(
ctx context.Context,
nsHostname string,
hostname string,
) (*NameserverResponse, error) {
return r.queryNameserver(ctx, nsHostname, hostname, recordTypes())
}
// queryNameserver queries a specific nameserver for the record types
// in qtypes and builds a NameserverResponse.
func (r *Resolver) queryNameserver(
ctx context.Context,
nsHostname string,
hostname string,
qtypes []uint16,
) (*NameserverResponse, error) {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
@@ -551,7 +578,7 @@ func (r *Resolver) QueryNameserver(
hostname = dns.Fqdn(hostname)
return r.queryAllTypes(ctx, nsHostname, nsIPs[0], hostname)
return r.queryTypes(ctx, nsHostname, nsIPs[0], hostname, qtypes)
}
// QueryNameserverIP queries a nameserver by its IP address directly,
@@ -568,14 +595,15 @@ func (r *Resolver) QueryNameserverIP(
hostname = dns.Fqdn(hostname)
return r.queryAllTypes(ctx, nsHostname, nsIP, hostname)
return r.queryTypes(ctx, nsHostname, nsIP, hostname, recordTypes())
}
func (r *Resolver) queryAllTypes(
func (r *Resolver) queryTypes(
ctx context.Context,
nsHostname string,
nsIP string,
hostname string,
qtypes []uint16,
) (*NameserverResponse, error) {
resp := &NameserverResponse{
Nameserver: nsHostname,
@@ -583,12 +611,6 @@ func (r *Resolver) queryAllTypes(
Status: StatusOK,
}
qtypes := []uint16{
dns.TypeA, dns.TypeAAAA, dns.TypeCNAME,
dns.TypeMX, dns.TypeTXT, dns.TypeSRV,
dns.TypeCAA, dns.TypeNS,
}
state := r.queryEachType(ctx, nsIP, hostname, qtypes, resp)
classifyResponse(resp, state)
@@ -779,18 +801,19 @@ func (r *Resolver) QueryAllNameservers(
return nil, err
}
return r.queryEachNS(ctx, nameservers, hostname)
return r.queryEachNS(ctx, nameservers, hostname, recordTypes())
}
func (r *Resolver) queryEachNS(
ctx context.Context,
nameservers []string,
hostname string,
qtypes []uint16,
) (map[string]*NameserverResponse, error) {
results := make(map[string]*NameserverResponse)
for _, ns := range nameservers {
resp, err := r.QueryNameserver(ctx, ns, hostname)
resp, err := r.queryNameserver(ctx, ns, hostname, qtypes)
// A query the context cut short says nothing about the
// nameserver, so it must not be returned as its failure.
@@ -835,9 +858,10 @@ func (r *Resolver) LookupAllRecords(
}
// ResolveIPAddresses resolves a hostname to all IPv4 and IPv6
// addresses, following CNAME chains up to MaxCNAMEDepth. When no
// nameserver of the name's zone answered, it returns an error rather
// than no addresses.
// addresses, following CNAME chains up to MaxCNAMEDepth. It asks each
// nameserver of the name's zone for its A, AAAA and CNAME records only.
// When no nameserver of the name's zone answered, it returns an error
// rather than no addresses.
func (r *Resolver) ResolveIPAddresses(
ctx context.Context,
hostname string,
@@ -858,7 +882,12 @@ func (r *Resolver) resolveIPWithCNAME(
return nil, ErrCNAMEDepthExceeded
}
results, err := r.QueryAllNameservers(ctx, hostname)
nameservers, err := r.FindAuthoritativeNameservers(ctx, hostname)
if err != nil {
return nil, err
}
results, err := r.queryEachNS(ctx, nameservers, hostname, addressTypes())
if err != nil {
return nil, err
}