Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot df68bbc615 watcher: send the inconsistency alert once per disagreement (closes #158)
check / check (push) Successful in 1m3s
detectInconsistencies alerted for neighbouring pairs of nameservers whose
records differed, on every DNS check, for as long as they differed. It now
also takes the previous hostname state, compares every pair of nameservers,
and alerts for a pair that differs unless both were in that state and
already differed there, so a nameserver new on a check that answers
differently is reported once. The state loaded at startup is the previous
state for the first check, so a disagreement saved before a restart is not
reported again. The choice of pairs is tested on record data, and the alert
through the hostname change detection with the notifier stand-in and no
resolver. The README describes the new behaviour.

Model: opus-5-5
2026-09-29 00:03:30 +00:00
4 changed files with 26 additions and 15 deletions
+9 -6
View File
@@ -74,12 +74,15 @@ rejected.
This is distinct from "responded with no records." This is distinct from "responded with no records."
- **NS recovery**: A previously-unreachable nameserver starts - **NS recovery**: A previously-unreachable nameserver starts
responding again. responding again.
- **Inconsistency detected**: Two nameservers that agreed on the - **Inconsistency detected**: Two nameservers return different record
previous check now return different record sets for the same sets for the same hostname and did not already differ on the previous
hostname. Every pair of nameservers is compared. The alert is sent check. Every pair of nameservers is compared. The alert is sent once
once for each such pair, on the check where they start to disagree, for each such pair, on the check where they start to disagree, and not
and not again while they keep disagreeing, including after a restart. again while they keep disagreeing, including after a restart. A
If they agree again and later disagree, it is sent again. nameserver that was not in the previous check (newly added, or back
after dropping out) and answers differently is reported on the check
where it appears. If a pair agrees again and later disagrees, the
alert is sent again.
### TCP Port Monitoring ### TCP Port Monitoring
+4 -3
View File
@@ -24,9 +24,10 @@ Rationale, Design, TODO, License, Author) if any are still missing.
# Completed Steps # Completed Steps
- 2026-09-28: the inconsistency alert is sent once, on the check where two - 2026-09-28: the inconsistency alert is sent once, on the check where two
nameservers that agreed start to disagree, instead of on every check while nameservers start to disagree or where a nameserver that disagrees first
they disagree, and not again after a restart. Every pair of nameservers is appears, instead of on every check while they disagree, and not again after
compared, not only neighbours in sorted order of name (closes #158). a restart. Every pair of nameservers is compared, not only neighbours in
sorted order of name (closes #158).
- 2026-09-28: DNS names in record values (CNAME, MX, SRV and NS targets) are - 2026-09-28: DNS names in record values (CNAME, MX, SRV and NS targets) are
lower-cased, so nameservers that answer in different letter case no longer lower-cased, so nameservers that answer in different letter case no longer
count as inconsistent or as a record change (closes #157). count as inconsistent or as a record change (closes #157).
+9 -3
View File
@@ -80,10 +80,10 @@ func TestNewlyDisagreeingPairs(t *testing.T) {
want: [][][2]string{nil, nil}, want: [][][2]string{nil, nil},
}, },
{ {
name: "nameserver new on this check does not alert", name: "nameserver new on the first check and disagreeing alerts once",
loaded: onlyA, loaded: onlyA,
checks: []map[string]map[string][]string{disagree}, checks: []map[string]map[string][]string{disagree, disagree},
want: [][][2]string{nil}, want: [][][2]string{alert, nil},
}, },
{ {
name: "disagreement after agreeing again alerts again", name: "disagreement after agreeing again alerts again",
@@ -123,6 +123,7 @@ func TestNewlyDisagreeingPairs(t *testing.T) {
func TestInconsistencyAlert(t *testing.T) { func TestInconsistencyAlert(t *testing.T) {
t.Parallel() t.Parallel()
onlyA := map[string]map[string][]string{nsA: {"A": {ip1}}}
agree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip1}}} agree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip1}}}
disagree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip2}}} disagree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip2}}}
@@ -143,6 +144,11 @@ func TestInconsistencyAlert(t *testing.T) {
loaded: disagree, loaded: disagree,
want: 0, want: 0,
}, },
{
name: "nameserver new on the first check and disagreeing alerts once",
loaded: onlyA,
want: 1,
},
} }
for _, tt := range tests { for _, tt := range tests {
+4 -3
View File
@@ -471,8 +471,9 @@ func (w *Watcher) detectInconsistencies(
} }
// newlyDisagreeingPairs returns every pair of nameservers whose records // newlyDisagreeingPairs returns every pair of nameservers whose records
// differ in current but were equal in prev, in sorted order of name. // differ in current, in sorted order of name, except pairs where both
// A pair with a nameserver missing from prev is not returned. // nameservers were in prev and already differed there. A nameserver
// missing from prev is paired with every nameserver it differs from.
func newlyDisagreeingPairs( func newlyDisagreeingPairs(
prev *state.HostnameState, prev *state.HostnameState,
current map[string]map[string][]string, current map[string]map[string][]string,
@@ -495,7 +496,7 @@ func newlyDisagreeingPairs(
prev1, ok1 := prev.RecordsByNameserver[ns1] prev1, ok1 := prev.RecordsByNameserver[ns1]
prev2, ok2 := prev.RecordsByNameserver[ns2] prev2, ok2 := prev.RecordsByNameserver[ns2]
if !ok1 || !ok2 || !recordsEqual(prev1.Records, prev2.Records) { if ok1 && ok2 && !recordsEqual(prev1.Records, prev2.Records) {
continue continue
} }