watcher: notify NS query failure and recovery (closes #104)
check / check (push) Successful in 1m4s

LookupAllRecords now returns each nameserver's response, so the
watcher saves its status: ok when it answered, NXDOMAIN and no records
included, and error with the reason when it timed out, answered
SERVFAIL or REFUSED, or could not be reached. A nameserver that starts
failing sends NS Failure and one that answers again sends NS Recovery.
A failing nameserver is left out of the record change and
inconsistency comparisons. The resolver used to report REFUSED and
network errors as an answer with no records; they are now errors. A
lookup cut short by its context now returns an error instead of a
failure of the nameserver it was querying.

Model: opus-5-5
This commit is contained in:
2026-10-01 19:35:54 +00:00
parent 6070356676
commit 1b7df5ecd7
10 changed files with 546 additions and 90 deletions
+10 -3
View File
@@ -687,11 +687,12 @@ func TestNSFailureAndRecovery(t *testing.T) {
cfg.Hostnames = []string{testHost}
// Between the checks, save every nameserver the first check found
// as failed, and add, as answering, one that live DNS does not list.
// as one that did not answer, and add, as answering, one that live
// DNS does not list, which then disappears.
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
hs, _ := deps.state.GetHostnameState(testHost)
for _, nsState := range hs.RecordsByNameserver {
nsState.Status = "error"
for ns := range hs.RecordsByNameserver {
hs.RecordsByNameserver[ns] = failed()
}
hs.RecordsByNameserver[oldNS1] = &state.NameserverRecordState{
@@ -704,4 +705,10 @@ func TestNSFailureAndRecovery(t *testing.T) {
assertNotified(t, deps, "NS Failure: "+testHost, "error")
assertNotified(t, deps, "NS Recovery: "+testHost, "success")
// A nameserver that did not answer has no records to compare, so
// its recovery is not also a record change.
if n := countNotifications(deps, "Record Change: "+testHost); n != 0 {
t.Errorf("sent %d record changes on recovery, want 0", n)
}
}