Skip to content

Connection alerts: announce already-down servers and re-alert during standing outages (#1659) - #1674

Merged
erikdarlingdata merged 3 commits into
devfrom
feature/1659-connection-alert-refire
Jul 26, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
feature/1659-connection-alert-refire

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Closes #1659 (split out of #1535). The issue was "blocked on one answer" only to size the minimum fix — Erik's proposed shape from the #1535 thread covers both cases, and this implements both, opt-in, defaults off.

The gap

Connection alerts were pure edge detection: an app/service started mid-outage never announced the outage (no edge existed), and a standing outage alerted exactly once — which silently broke the reporter's webhook-driven auto-heal loop the day the app restarted while the server was down.

The shape

  • One shared definition: ConnectionAlertPolicy in PerformanceMonitor.Common replaces Lite's ConnectionEdgeDetector and the inline machine in Darling's DarlingSelfAlertEvaluator (the SqlErrorClassification discipline — the two implementations are how these rules would drift). Pinned by identical test matrices in both suites.
  • Opt-in 1 — alert at first sight: a server already down on its first-ever observation announces instead of silently baselining.
  • Opt-in 2 — re-alert every N minutes while still down (0 = off, clamped ≤1440). Re-fires deliver under the same Server Unreachable metric name deliberately — webhook automation keyed on the metric re-triggers, which is the point — with the detail text marking the flavor.
  • Interlock: after a mid-outage restart, re-fire alone re-announces (a re-baselined outage has no recorded down alert → due immediately) even with the startup opt-in off. The re-fire clock stamps on delivery only, so an alert suppressed by the notify toggles never consumes the window (pinned by test).

Surfaces

  • Lite: settings.json keys + two Settings-window controls beside the existing connection toggle.
  • Darling: V33 columns on config_alert_settings (NOT NULL DEFAULT, additive) read live like the V20 toggle; seeded by StoreConfigProvider; editable in the viewer Settings window; the viewer's connect-time schema probe gains the V33 sentinel arm (mid-state pinned: tags-but-no-knobs reads 32). No ACL/provisioning change — the table carries table-level grants, no column carve.

Tests

Lite 1479/0, Darling 3114/0 locally. New: policy matrix ×2 suites, evaluator startup-fire + interval re-fire + stamp-on-delivery-only (via the existing utcNow seam), V33 migration pins, probe-arm pins.

Field note: deploying this to DARLING01 applies V33 on service start (deploy sweep queued after merge).

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits July 26, 2026 08:30
…standing outages (#1659)

Connection alerts were pure edge detection - correct in isolation, wrong for
the reporter's webhook-driven auto-heal loop: an app/service started mid-outage
never announced the outage (no edge existed), and a standing outage alerted
exactly once however long it lasted.

Two OPT-INS, both default-off (classic behavior untouched), one shared
definition (ConnectionAlertPolicy in PerformanceMonitor.Common - replaces
Lite's ConnectionEdgeDetector and the inline machine in Darling's
DarlingSelfAlertEvaluator; pinned from both suites, the SqlErrorClassification
discipline):

- Alert at first sight: a server already down on its first-ever observation
  announces instead of silently baselining.
- Re-alert every N minutes while still down (0 = off). Re-fires deliver under
  the SAME "Server Unreachable" metric name so metric-keyed webhook automation
  re-triggers; the detail text marks the flavor. The re-fire clock stamps on
  DELIVERY only, so a decision suppressed by the notify toggles never consumes
  the window - and after a mid-outage restart, re-fire alone re-announces
  (no recorded down alert = due immediately) even with the startup opt-in off.

Lite: settings.json + Settings window knobs beside the connection toggle.
Darling: V33 columns on config_alert_settings (read live like the V20 toggle,
refire clamped 0-1440), seeded by StoreConfigProvider, editable in the
viewer's Settings window; schema-gate probe gains the V33 sentinel arm.
No ACL/provisioning change - config_alert_settings carries table-level grants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 72e2e7c into dev Jul 26, 2026
5 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/1659-connection-alert-refire branch July 26, 2026 13:26
erikdarlingdata added a commit that referenced this pull request Jul 26, 2026
Four store-polled self-alerts over the AG collectors' latest snapshot, evaluated
on the existing per-server self-alert sweep:

  AG Failover              (Warning)  role_desc changed since the previous sweep
  AG Replica Disconnected  (Critical) connected_state_desc crossed DISCONNECTED
  AG Replica Reconnected   (resolved) the recovery notice for the above
  AG Sync Fell Behind      (Warning)  secondary_lag_seconds or redo_queue_size
  AG Database Suspended    (Warning)  is_suspended false -> true, with the reason

State machines live in DarlingSelfAlertEvaluator behind injectable Func seams,
keyed per AG grain (ag+replica, ag+database+replica) rather than per server, so
two lagging databases on one host track and recover independently. Every alert
still fires under the real server_id as its serverKey, so per-server delivery
overrides, mute rules and history correlation are unaffected.

First sighting of any replica or database is a silent baseline (the
ConnectionAlertPolicy discipline), and a NULL state string is skipped rather
than read as a transition - WSFC quorum loss nulls the AG catalog views
wholesale, and treating that as an edge would spray alerts across the fleet at
the worst possible moment.

JudgeAgSync returns three states, not a bool, and that is the load-bearing
design decision. secondary_lag_seconds reads 0 - not NULL - while data movement
is SUSPENDED, so the seconds trigger has to abstain on a suspended row. If
abstaining were a plain "not behind", the caller would read it as recovery: a
lagging database that got SUSPENDED, or whose columns went NULL under quorum
loss, would have emitted "AG Sync Recovered - has caught up with the primary" in
the same sweep that reported it suspended. Only a database this sweep MEASURED
as caught up resolves a standing alert. That also makes cross-server resolution
structurally impossible rather than something a prefix check has to catch.

Store-backed settings (V35 on config.config_alert_settings, all NOT NULL
DEFAULT): notify_ag_health (true), ag_lag_alert_seconds (300, clamp 0-86400),
ag_redo_queue_alert_kb (0 = off, clamp 0-1073741824). Wired through
DarlingConfig, DarlingAlertSettings, StoreConfigProvider, ViewerDataService and
the viewer Settings window on the #1674 pattern; read live, so an edit takes
effect on the next sweep with no restart.

Each grain is read by its own command and gated on its OWN snapshot time. The
two were briefly read as two statements over one command to save a round trip;
that cannot work. Npgsql only splits multi-statement text into a batch when it
parses the SQL for NAMED placeholders - with the POSITIONAL ($1) parameters
every read in this file uses, it sends one extended-protocol Parse and
PostgreSQL rejects it with "cannot insert multiple commands into a prepared
statement". Failure-isolated as that path is, the whole family would have been
silently dead with one logged error per server per sweep. Per-grain gating is
also correct on its own merits: the AG collectors are scheduled independently,
so a healthy replica-grain timestamp must not vouch for stale database-grain
rows. The read is skipped entirely when the master switch is off, so an AG-free
fleet pays nothing either way.

Also fixes four pieces of adjacent drift found while wiring this up:

- AlertMetricClassifier.IsResolution recognized only Cleared/Resolved/Restored,
  so Darling's "Collection Resumed", "Agent Restarted" and "Compression Job
  Recovered" recoveries have been shipping styled as live actionable alerts in
  both apps' Alert History grids. Added Resumed/Restarted/Recovered/Reconnected
  rather than letting the AG recoveries become a fifth blind spot; no actionable
  metric name in either app contains those words. Both hand-maintained SQL
  copies of that suffix list (Darling's DailySummarySql, Lite's
  LocalDataService.DailySummary) are widened to match - they drive the Daily
  Summary actionable-alert counts in BOTH apps.
- Registered all five AG metric names in the shared AlertSeverity map so a
  renderer reaching it without an explicit severity override does not fall
  through to INFO-blue (the #1136 gap).
- get_alert_settings (MCP) stopped at 36 columns while the store grew to 41, so
  an MCP client could not see the V33 connection opt-ins at all. Extended to the
  full 41. ViewerControlPlaneStage3bTests - the parity test that pins the column
  list against the upsert placeholders, the bind order and the reader ordinals,
  i.e. the guard for the highest-risk defect class in this plumbing - had
  drifted the same way; extended, and its placeholder loop now runs off
  Columns.Length so the literal cannot drift again.
- The Darling suite had a real ~1-in-6 intermittent failure, and it was not the
  AG work: ViewerTimeHelper keeps the display mode and UTC offset in
  process-wide statics, three test classes must mutate them, and their shared
  "viewer-time-statics" collection had NO CollectionDefinition - so it grouped
  its members without constraining them, and xUnit ran it in parallel with every
  other collection. Any viewer test rendering a timestamp could observe a
  swapped mode mid-assertion; the victim differed run to run
  (ViewerSystemEventsTests, then ViewerWave3DisplayTests), which is the shape
  that reads as "flaky, just re-run it". Added the definition with
  DisableParallelization, which constrains the three mutators instead of chasing
  an open-ended set of readers. Verified with 12 consecutive full-suite runs.

TestResults/ is gitignored - trx logs from that investigation would otherwise
land in commits.

The two migration pin tests are re-pinned by identity rather than by ordinal
and literal count - the same fact stated twice made every stacked branch
collide on them.

Suites green: Darling 3194, Lite 1494, Dashboard 768.

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