Alert on a suspended secondary that is falling behind (#991) - #1700
Merged
Conversation
The lag trigger made the seconds check ABSTAIN on a suspended row. That was written to MS Learn, which says secondary_lag_seconds "shows as 0 if the data movement is suspended" - so abstaining looked like the careful choice, since a zero would otherwise read as "caught up" on the database that is furthest behind. The documentation is wrong. Measured against the live Docker AG fixture (SQL Server 2022 16.0.4265.3, clusterless AG, write load, sampled across a SUSPEND_FROM_USER on the secondary), lag ACCRUES monotonically at wall-clock rate while suspended and returns to 0 on resume: is_suspended=1 lag=3993 redo=0 send=NULL NOT SYNCHRONIZING is_suspended=1 lag=4005 redo=0 send=NULL NOT SYNCHRONIZING is_suspended=1 lag=4017 redo=0 send=NULL NOT SYNCHRONIZING is_suspended=1 lag=4029 redo=0 send=NULL NOT SYNCHRONIZING is_suspended=1 lag=4041 redo=0 send=NULL NOT SYNCHRONIZING after RESUME : lag=0 redo=0 send=0 SYNCHRONIZED +48 seconds of lag across four 12-second intervals. So the abstention was not being careful, it was silencing the alert on suspended data movement - the single most common way a secondary falls behind, and the case an operator most needs to be paged for. A suspended secondary would have drifted arbitrarily far behind while only "AG Database Suspended" fired once, on the edge. Replaced with an asymmetry: a SUSPENDED row may raise an alarm but may never clear one. This is deliberately correct under BOTH behaviors rather than betting on the measurement. If lag accrues (measured), a suspended secondary crosses the threshold and fires. If it ever did read 0 (documented), that 0 is under the threshold and yields NotMeasurable rather than CaughtUp, so it still cannot resolve a standing alert - which is the false-recovery bug the three-state judgement was introduced for. The same rule now also protects the redo trigger, whose value FREEZES at its last reading while suspended (also measured): frozen and over the threshold is a real backlog worth firing on, frozen and under it is stale data that must not clear anything. Previously a small frozen redo queue could resolve a standing alert. Also documented, both measured on the fixture and neither a code change: log_send_queue_size reads NULL while suspended rather than growing, so it is useless as a fell-behind signal (this evaluator never used it); and on RESUME the secondary has a real backlog to drain (388,620 KB after a 60-second suspend under load), so a single-sample redo threshold fires during legitimate catch-up - not wrong, since the data-loss window is genuinely open until it drains, but it is why that trigger ships OFF. Plus a vantage note on the evaluator: sys.dm_hadr_* on a SECONDARY carries only that replica's own rows, so a monitored secondary sees a one-row self-view of its AG. Nothing here assumed otherwise, but full coverage needs the primary monitored. Field evidence from ag-fixture-builder and ag-collector-builder (tools/ag-fixture/VALIDATION.md); I re-ran the suspend/resume cycle against the running fixture myself before changing shipped logic, and left it SYNCHRONIZED. Darling suite green: 3252 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erikdarlingdata
enabled auto-merge
July 26, 2026 19:05
erikdarlingdata
added a commit
that referenced
this pull request
Jul 26, 2026
The comment restated MS Learn's "shows as 0 if the data movement is suspended" as fact and reasoned from it. The sentence is wrong on 16.0.4265.3, and this is not academic: the AG alert family shipped a rule that ABSTAINED on a suspended row specifically to compensate for the documented behavior, which silenced the lag alert on suspended data movement - the most common way a secondary falls behind. That is fixed separately in #1700. Replaced with what the column was measured doing, twice, by two parties: reads 0 while movement is ACTIVE and caught up, accrues once suspended, and on a suspended row reports roughly how stale the secondary's last hardened log is rather than time since suspension - so it starts near zero under load but at thousands of seconds on an idle group. It also does not latch immediately; a suspended row can still read 0 for a sample or two, which is why a suspended row may raise a lag alarm but must never clear one. Comment only, no behavior change. The query and the collected columns were already correct - both were validated verbatim against the fixture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 tasks
pull Bot
pushed a commit
to ehtick/PerformanceMonitor
that referenced
this pull request
Jul 29, 2026
Follow-up to erikdarlingdata#1700. That PR got the RULE right but the MECHANISM wrong, and the mechanism is what an operator needs to tune a threshold. ag-fixture-builder reconciled why their measurement started near 0 and mine started near 3993: theirs was under write load, mine was idle. While suspended the column reports the staleness of the last hardened log (roughly now - last_hardened_time), which then grows at wall-clock rate. It is NOT time since suspension. Under load the last hardening is near-now so it starts around 0 and climbs; on an idle group it starts at however long since the last write, so it can jump straight to a large number. Same behavior, different starting points - neither measurement was wrong. Chasing that turned up something sharper, which I measured myself on the idle fixture rather than taking second-hand. Sampled every 15 seconds across a 60-second suspend with no write load: elapsed susp lag redo send state secs_since_hardened 0 1 0 0 NULL NOT SYNCHRONIZING 262 15 1 0 0 NULL NOT SYNCHRONIZING 277 30 1 0 0 NULL NOT SYNCHRONIZING 292 45 1 0 0 NULL NOT SYNCHRONIZING 307 60 1 0 0 NULL NOT SYNCHRONIZING 322 Lag read 0 at EVERY sample while the replica was already NOT SYNCHRONIZING and its last hardened log aged past five minutes. It did not latch late, it never latched. (ag-fixture-builder saw it latch at +30s on their idle run, so the timing varies; the operational fact does not.) Two things follow, and erikdarlingdata#1700's asymmetry already survives both - this commit is documentation and alert text, no logic change: 1. A suspended replica can report zero lag for an entire outage. "A sub-threshold reading on a suspended row yields NotMeasurable, never CaughtUp" is what stops that zero from clearing a standing alarm on a replica receiving nothing. The rule was built for the inverted-docs case and turns out to be load-bearing for a completely different and more common one. 2. THE LAG TRIGGER ALONE CANNOT DETECT SUSPENDED DATA MOVEMENT on a quiet group. "AG Database Suspended" is the alert that owns that case. Worth stating plainly, because the obvious assumption is that a lag threshold covers it. The alert detail text now also says what the number is: staleness of the last hardened log, not volume of queued data. On a quiet group a large lag can simply mean nothing has been written recently. The volume measure would be log_send_queue_size, and that reads NULL while suspended - so there is no queued-bytes figure available for a suspended replica at all. Darling suite green: 3252 passed. Fixture left SYNCHRONIZED. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Corrects a rule I shipped in #1692 that silenced the alert on the most common way a secondary falls behind.
What was wrong
The lag trigger made the seconds check abstain on a suspended row. That was written to MS Learn, which states
secondary_lag_seconds"shows as 0 if the data movement is suspended" -- so abstaining looked like the careful choice, since a zero would otherwise read as "caught up" on the database that is furthest behind.The documentation is wrong. ag-fixture-builder measured the inverse on the live Docker AG fixture, ag-collector-builder confirmed it, and I re-ran the suspend/resume cycle myself before changing shipped logic (SQL Server 2022 16.0.4265.3, clusterless AG, write load, sampled across a
SUSPEND_FROM_USERon the secondary):+48 seconds of lag across four 12-second intervals -- it accrues at wall-clock rate and returns to 0 on resume. So the abstention was not being careful, it was silencing the alert on suspended data movement. A suspended secondary could drift arbitrarily far behind while only
AG Database Suspendedfired once, on the edge.The fix
A suspended row may raise an alarm but may never clear one.
This is deliberately correct under both behaviors rather than betting on the measurement:
0(documented), that0is under the threshold and yieldsNotMeasurablerather thanCaughtUp, so it still cannot resolve a standing alert -- which is the false-recovery bug the three-state judgement was introduced for in the first place.The same rule now also protects the redo trigger, whose value freezes at its last reading while suspended (also measured). Frozen and over the threshold is a real backlog worth firing on; frozen and under it is stale data that must not clear anything. Previously a small frozen redo queue could resolve a standing alert.
Documented, not coded
log_send_queue_sizereads NULL while suspended rather than growing, so it is useless as a fell-behind signal. This evaluator never used it -- noted so nobody reaches for it later.sys.dm_hadr_*on a secondary carries only that replica's own rows, so a monitored secondary sees a one-row self-view of its AG. Nothing here assumed otherwise (the rules judge whatever rows arrive, keyed per ag+replica), but full AG coverage needs the primary monitored.Not taken
The fixture also validated that replica-grain
synchronization_health_descflipsHEALTHY->NOT_HEALTHYduring a suspend, which is a usable signal. I did not add a fifth alert for it -- it would largely duplicateAG Database Suspended, and the family was scoped to four. Worth considering as its own follow-up.Testing
Darling suite green: 3252 passed, 0 failed. The pure judgement gains a case for every quadrant (suspended over/under threshold, for both triggers, plus the resumed measurement that does resolve), and there is a new end-to-end test for the exact case the old rule silenced: a suspended secondary drifting past the threshold now fires, stays quiet inside the cooldown, and resolves only once movement is running again.
The fixture was left
SYNCHRONIZEDand healthy.🤖 Generated with Claude Code