Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot 8ea6191566 watcher: send the inconsistency alert once per disagreement (closes #158)
check / check (push) Successful in 1m19s
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 only when both were in that state with equal records.
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 itself 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-28 23:42:56 +00:00
4 changed files with 15 additions and 26 deletions
+6 -9
View File
@@ -74,15 +74,12 @@ 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 return different record - **Inconsistency detected**: Two nameservers that agreed on the
sets for the same hostname and did not already differ on the previous previous check now return different record sets for the same
check. Every pair of nameservers is compared. The alert is sent once hostname. Every pair of nameservers is compared. The alert is sent
for each such pair, on the check where they start to disagree, and not once for each such pair, on the check where they start to disagree,
again while they keep disagreeing, including after a restart. A and not again while they keep disagreeing, including after a restart.
nameserver that was not in the previous check (newly added, or back If they agree again and later disagree, it is sent again.
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
+3 -4
View File
@@ -24,10 +24,9 @@ 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 start to disagree or where a nameserver that disagrees first nameservers that agreed start to disagree, instead of on every check while
appears, instead of on every check while they disagree, and not again after they disagree, and not again after a restart. Every pair of nameservers is
a restart. Every pair of nameservers is compared, not only neighbours in compared, not only neighbours in sorted order of name (closes #158).
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).
+3 -9
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 the first check and disagreeing alerts once", name: "nameserver new on this check does not alert",
loaded: onlyA, loaded: onlyA,
checks: []map[string]map[string][]string{disagree, disagree}, checks: []map[string]map[string][]string{disagree},
want: [][][2]string{alert, nil}, want: [][][2]string{nil},
}, },
{ {
name: "disagreement after agreeing again alerts again", name: "disagreement after agreeing again alerts again",
@@ -123,7 +123,6 @@ 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}}}
@@ -144,11 +143,6 @@ 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 {
+3 -4
View File
@@ -471,9 +471,8 @@ func (w *Watcher) detectInconsistencies(
} }
// newlyDisagreeingPairs returns every pair of nameservers whose records // newlyDisagreeingPairs returns every pair of nameservers whose records
// differ in current, in sorted order of name, except pairs where both // differ in current but were equal in prev, in sorted order of name.
// nameservers were in prev and already differed there. A nameserver // A pair with a nameserver missing from prev is not returned.
// 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,
@@ -496,7 +495,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
} }