Skip to content

AG fixture: land the suspended-row evidence, and correct the lag latch claim (#991) - #1707

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/991-ag-suspended-surface-evidence
Jul 26, 2026
Merged

AG fixture: land the suspended-row evidence, and correct the lag latch claim (#991)#1707
erikdarlingdata merged 2 commits into
devfrom
feature/991-ag-suspended-surface-evidence

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Evidence file and CHANGELOG only, no code. Two files.

#1702 and #1703 both cite tools/ag-fixture/VALIDATION.md for how a suspended row behaves, and the file did not contain the measurements behind several of those claims. This lands them - and corrects one of mine.

Correction to #1704

I wrote that secondary_lag_seconds "only latched at the +30s sample", which implied a short bounded window. That was too generous. @ag-alerts-builder measured it never latching at all across a full 60-second suspension on a quiet group, and I reproduced it independently rather than take it on report:

elapsed  is_suspended  sync_state         lag  secs_since_hardened
healthy  0             SYNCHRONIZED       0    1757
0s       1             NOT SYNCHRONIZING  0    1757
+15s     1             NOT SYNCHRONIZING  0    1772
+30s     1             NOT SYNCHRONIZING  0    1788
+45s     1             NOT SYNCHRONIZING  0    1803
+60s     1             NOT SYNCHRONIZING  0    1818
resume   0             SYNCHRONIZED       0    16

Zero at every sample, already NOT SYNCHRONIZING, last hardened log ~29 minutes stale. One earlier idle run latched at +30s and these did not, which is the actual lesson: the timing is not dependable in either direction and nothing should be built on it.

Stated plainly in the file now, because the intuitive assumption is the opposite:

A lag threshold alone cannot detect suspended data movement on a quiet group. It can read 0 for the entire outage.

The dedicated suspended-state alert owns that case. This is what makes #1700's "a sub-threshold reading on a suspended row is never caught up" load-bearing for a far more common scenario than the inverted-docs one it was written for.

Also landed (new section 6b)

With sample tables from a loaded run and an idle one:

  • All four *_time columns freeze at their last pre-suspension instant, so a cross-replica commit-time delta stops growing exactly when replication stops - understating the problem at the moment it is worst, the opposite direction from secondary_lag_seconds.
  • redo_queue_size freezes (21896 KB flat) rather than growing.
  • est_redo_completion_time_min is the sharpest edge. Queue over rate with both frozen holds a small static reassuring value for the whole suspension - 0.0144 min under load, 0 when idle - so a suspended replica looks healthier than a working one. est_send_drain_time_min at least reads NULL.
  • last_received_time read NULL in every sample, healthy and suspended alike, on both runs. Optional rather than expected.
  • last_commit_time is not a heartbeat. Measured 1757 seconds behind wall clock on a SYNCHRONIZED, non-suspended, zero-lag replica, purely because the database was quiet. So now - last_commit_time is not a lag measure - on a healthy idle replica it grows without bound, and anything alerting on it pages about an idle database. This one bites on healthy replicas, not just suspended ones.

The summary table at the top is updated so it no longer contradicts any of this.

Attribution

The loaded-run table is @ag-collector-builder's, supplied as raw data; the idle run, the never-latch reproduction, and the last_commit_time baseline are mine. @ag-alerts-builder found the never-latch case first.

Test plan

  • Never-latch reproduced independently on the fixture rather than accepted on report - it contradicted my own published claim, so it needed verifying.
  • *_time freeze, last_received_time NULL, and the last_commit_time gap all confirmed in my own idle run as well.
  • CHANGELOG: entry plus link-ref, no duplicate [#1702]/[#1703] refs, no conflict markers.
  • Diff against dev is exactly CHANGELOG.md + tools/ag-fixture/VALIDATION.md - no collector or app code, so it cannot conflict with anything in flight.

Fixture left as found: both replicas SYNCHRONIZED, is_suspended = 0, healthy.

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits July 26, 2026 15:56
…ag claim

#1702 and #1703 both cite tools/ag-fixture/VALIDATION.md for how a suspended
row behaves, and the file did not contain the measurements behind several of
those claims. This lands them.

It also corrects something I got wrong in #1704. I wrote that
secondary_lag_seconds "only latched at the +30s sample", which implied a short
bounded window. ag-alerts-builder measured it never latching across a whole
60s suspension on a quiet group, and I reproduced that independently here: 0
at every sample through a full 60-second suspension, already NOT
SYNCHRONIZING, with the last hardened log ~29 minutes stale. One earlier idle
run latched at +30s and these did not, so the timing is not dependable in
either direction. The file now says so, and states the consequence plainly: a
lag threshold alone cannot detect suspended data movement on a quiet group.

Also added, with sample tables from a loaded run and an idle one:

- All four *_time columns freeze at their last pre-suspension instant, so a
  cross-replica commit-time delta stops growing exactly when replication
  stops - the opposite direction from secondary_lag_seconds.
- redo_queue_size freezes rather than growing.
- est_redo_completion_time_min is the sharpest edge: queue over rate with both
  frozen holds a small static reassuring number for the whole suspension
  (0.0144 min loaded, 0 idle), so a suspended replica looks healthier than a
  working one.
- last_received_time read NULL in every sample on both runs.
- last_commit_time is not a heartbeat - measured 1757 seconds behind wall
  clock on a SYNCHRONIZED, non-suspended, zero-lag replica, purely because the
  database was quiet.

Loaded-run table is ag-collector-builder's; the idle run and the never-latch
reproduction are mine. Evidence file and CHANGELOG only, no code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PR came out as 1707, not the 1705 predicted at write time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 886d621 into dev Jul 26, 2026
4 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/991-ag-suspended-surface-evidence branch July 26, 2026 20:19
pull Bot pushed a commit to ehtick/PerformanceMonitor that referenced this pull request Jul 29, 2026
Non-executable changes now skip .NET setup, restore and versioning in the
required build job. The job still RUNS, so the check reports a result and
cannot go missing and block a merge.

The gate is an explicit ALLOWLIST of non-executable content, not a
subtraction of known-code paths: **/*.md, LICENSE, CITATION.cff,
.gitignore, .gitattributes, docs/**, Screenshots/**. An unfamiliar new
file type therefore defaults to being treated as code, which is the safe
direction to be wrong in.

Deliberately NOT in the allowlist: *.sql (the installer and
sql-validation compile it), *.yml (workflows), *.csproj / *.props /
packages.lock.json (build inputs), and *.cs however comment-only a change
looks - an XML doc comment still recompiles, and the compiler is what
proves it still builds.

Two guards against silently under-building:

- The fast path never engages on a release event, or on a push to
  dev/main. Those restore unconditionally. Note this only forces the
  RESTORE back on; the per-product build/test steps stay path-gated
  exactly as before, so push times are unchanged for code commits.
- Both jobs emit a ::notice:: naming why they took the path they took,
  and the build job lists the files it classified as documentation. A job
  reporting success having quietly run nothing is otherwise
  indistinguishable from one that tested everything.

On what this actually fixes: markdown-only changes were ALREADY skipping
every heavy step - erikdarlingdata#1707 was pure .md and its build ran in 1m43s, which
is checkout and the paths filter with nothing after it. The real gap was
non-markdown documentation, which matched the '**' catch-all in the code
filter and paid a six-project locked-mode restore to compile nothing. The
remaining ~1m45s floor is actions/checkout on a Windows runner and is not
something this gate can remove.

The docs allowlist and the code filter's exclusions are the same list
stated twice - paths-filter has no "not in that other filter" operator -
so they carry a comment to keep them in step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant