resolver, watcher: a record type whose query fails keeps its previous records (closes #231)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The resolver lists in FailedTypes each record type whose query to a nameserver got no usable reply: none after two tries, an error reply, a referral, or a truncated reply whose TCP retry failed. Records holds nothing for such a type, never none or the part that fit. A nameserver that answered no type has failed, as before. The watcher keeps the previous check's records for a failed type; when that check did not know them either (first check, new or failing nameserver), the type is saved in failedTypes and left out of record and inconsistency comparisons. ResolveIPAddresses no longer counts a nameserver whose A, AAAA or CNAME query failed as an answer. Model: opus-5-5
This commit is contained in:
+60
-11
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -266,9 +267,9 @@ func (w *Watcher) checkDomain(
|
||||
return
|
||||
}
|
||||
|
||||
newState := buildHostnameState(results, now)
|
||||
|
||||
prevHS, hasPrevHS := w.state.GetHostnameState(domain)
|
||||
newState := buildHostnameState(results, prevHS, now)
|
||||
|
||||
if hasPrevHS && !w.firstRun {
|
||||
w.detectHostnameChanges(ctx, domain, prevHS, newState)
|
||||
}
|
||||
@@ -398,9 +399,9 @@ func (w *Watcher) checkHostname(
|
||||
return
|
||||
}
|
||||
|
||||
newState := buildHostnameState(results, time.Now().UTC())
|
||||
|
||||
prev, hasPrev := w.state.GetHostnameState(hostname)
|
||||
newState := buildHostnameState(results, prev, time.Now().UTC())
|
||||
|
||||
if hasPrev && !w.firstRun {
|
||||
w.detectHostnameChanges(ctx, hostname, prev, newState)
|
||||
}
|
||||
@@ -411,9 +412,11 @@ func (w *Watcher) checkHostname(
|
||||
// buildHostnameState saves each nameserver's response. A nameserver
|
||||
// that answered, even with NXDOMAIN or no records, is saved as ok; one
|
||||
// that timed out or failed is saved as error with the reason, and its
|
||||
// empty record set is not an answer.
|
||||
// empty record set is not an answer. prev is the hostname's state from
|
||||
// the previous check, or nil.
|
||||
func buildHostnameState(
|
||||
results map[string]*resolver.NameserverResponse,
|
||||
prev *state.HostnameState,
|
||||
now time.Time,
|
||||
) *state.HostnameState {
|
||||
hs := &state.HostnameState{
|
||||
@@ -425,7 +428,7 @@ func buildHostnameState(
|
||||
|
||||
for ns, resp := range results {
|
||||
nsState := &state.NameserverRecordState{
|
||||
Records: resp.Records,
|
||||
Records: maps.Clone(resp.Records),
|
||||
Status: statusOK,
|
||||
LastChecked: now,
|
||||
}
|
||||
@@ -434,6 +437,13 @@ func buildHostnameState(
|
||||
resp.Status == resolver.StatusError {
|
||||
nsState.Status = statusError
|
||||
nsState.Error = resp.Error
|
||||
} else {
|
||||
var prevNS *state.NameserverRecordState
|
||||
if prev != nil {
|
||||
prevNS = prev.RecordsByNameserver[ns]
|
||||
}
|
||||
|
||||
keepFailedTypes(nsState, prevNS, resp.FailedTypes)
|
||||
}
|
||||
|
||||
hs.RecordsByNameserver[ns] = nsState
|
||||
@@ -442,6 +452,30 @@ func buildHostnameState(
|
||||
return hs
|
||||
}
|
||||
|
||||
// keepFailedTypes copies into nsState, for each record type in
|
||||
// failedTypes, whose query to the nameserver failed, the records prevNS,
|
||||
// the nameserver's state from the previous check, holds for that type,
|
||||
// which may be none. When prevNS does not know them either, because the
|
||||
// nameserver was new or failing then or the type was in its
|
||||
// FailedTypes, the type goes in nsState.FailedTypes instead.
|
||||
func keepFailedTypes(
|
||||
nsState, prevNS *state.NameserverRecordState,
|
||||
failedTypes []string,
|
||||
) {
|
||||
for _, rtype := range failedTypes {
|
||||
if prevNS == nil || prevNS.Status != statusOK ||
|
||||
slices.Contains(prevNS.FailedTypes, rtype) {
|
||||
nsState.FailedTypes = append(nsState.FailedTypes, rtype)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if records, ok := prevNS.Records[rtype]; ok {
|
||||
nsState.Records[rtype] = records
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) detectHostnameChanges(
|
||||
ctx context.Context,
|
||||
hostname string,
|
||||
@@ -467,7 +501,7 @@ func (w *Watcher) detectRecordChanges(
|
||||
continue
|
||||
}
|
||||
|
||||
if recordsEqual(prevNS.Records, cur.Records) {
|
||||
if sameRecords(prevNS, cur) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -600,9 +634,9 @@ func newlyDisagreeingPairs(
|
||||
|
||||
for i, ns1 := range nameservers {
|
||||
for _, ns2 := range nameservers[i+1:] {
|
||||
if recordsEqual(
|
||||
current.RecordsByNameserver[ns1].Records,
|
||||
current.RecordsByNameserver[ns2].Records,
|
||||
if sameRecords(
|
||||
current.RecordsByNameserver[ns1],
|
||||
current.RecordsByNameserver[ns2],
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -612,7 +646,7 @@ func newlyDisagreeingPairs(
|
||||
|
||||
if ok1 && ok2 &&
|
||||
prev1.Status == statusOK && prev2.Status == statusOK &&
|
||||
!recordsEqual(prev1.Records, prev2.Records) {
|
||||
!sameRecords(prev1, prev2) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1090,6 +1124,21 @@ func toSet(items []string) map[string]bool {
|
||||
return set
|
||||
}
|
||||
|
||||
// sameRecords reports whether two nameserver states hold the same
|
||||
// records, leaving out the record types either lists in FailedTypes:
|
||||
// their records are not known.
|
||||
func sameRecords(a, b *state.NameserverRecordState) bool {
|
||||
aRecords := maps.Clone(a.Records)
|
||||
bRecords := maps.Clone(b.Records)
|
||||
|
||||
for _, rtype := range slices.Concat(a.FailedTypes, b.FailedTypes) {
|
||||
delete(aRecords, rtype)
|
||||
delete(bRecords, rtype)
|
||||
}
|
||||
|
||||
return recordsEqual(aRecords, bRecords)
|
||||
}
|
||||
|
||||
func recordsEqual(
|
||||
a, b map[string][]string,
|
||||
) bool {
|
||||
|
||||
Reference in New Issue
Block a user