Repository navigation
Postgres fleet (pgmon) has no per-target operational alerting — only tool-health and 3 outage-predictor alerts fire #2711
Description
Summary
Part of a broader alerting-integration effort scoped 2026-08-31 (headless notification-config exposure and Datadog-parity payload structure are two sibling issues being filed in parallel).
Tonight, SQL Server per-target alerts (Deadlocks Detected/Cleared, Blocking Detected/Cleared, High CPU/CPU Resolved, Poison Wait, Long-Running Query/Cleared, Failed Agent Job) fired repeatedly and correctly on the use1/use2 fleets. The Postgres fleet (pgmon) never produced a single one of these — every pgmon alert observed was tool-health (Collection Stopped, Collector Cost Regression, Store Job Over Cadence). This is not because Postgres alerting is unwired — it's a real but narrower and differently-shaped gap than "Postgres has nothing."
Confirmed mechanism (traced, not guessed)
EvaluateAlertsAsync (Darling/PerformanceMonitor.Darling.Service/DarlingWorker.cs:2724, called unconditionally from ProcessServerSweepAsync at :1718 for every connected server every 30s, no engine check at either call site) does two things:
-
await engine.EvaluateServerAsync(snapshot, cancellationToken)— the sharedAlertEngine, which evaluates Deadlocks/Blocking/CPU/Poison-Wait/Long-Running-Query/Failed-Agent-Job for every server regardless of engine. Its read adapter (DarlingAlertReadAdapter.cs) queries exclusively SQL-Server-only-populated tables:- Deadlocks →
FROM deadlocks(:247) - Blocking →
FROM dmv_blocking_snapshots/FROM blocked_process_reports(:106,:83,:201) - Poison Wait →
FROM wait_stats(:296) - Long-Running Query →
FROM query_snapshots(:356) - Failed Agent Job →
FROM running_jobs(:710) - High CPU → a separate read,
ReadLatestCpuAsync(DarlingWorker.cs:2865),FROM cpu_utilization_statsselectingsqlserver_cpu_utilization/other_process_cpu_utilization— column names are SQL-Server-specific.
None of these tables are ever written by a Postgres collector (confirmed: the PG collector set uses entirely distinct table names —
pg_deadlocks,pg_wait_stats,pg_kernel_stats, etc. — verified viaTargetTable =>onPgDeadlocksCollector.cs:102,PgKernelStatsCollector.cs:108,PgWaitStatsCollector.cs:125). So for a Postgresserver_id, every one of these queries returns zero rows — there is no explicit engine gate inAlertEngine/DarlingAlertReadAdapter; the effective exclusion happens entirely because the collector-level engine gate (CollectorCatalog.AppliesTo) already stops SQL-Server-only collectors from ever writing rows for a Postgres server_id. The alert-side code doesn't know or care that it's being asked a question its data source can never answer. - Deadlocks →
-
if (runtime.Target.Engine == CollectorTargetEngine.PostgreSql) await EvaluatePostgresAlertsAsync(...)(DarlingWorker.cs:2748-2753) — a real, already-shipped, tested Postgres-specific alert path, gated correctly on engine, evaluating three "Tier 0 outage predictors" viaPostgresAlertEvaluator.Evaluate(PerformanceMonitor.Alerting/PostgresAlertEvaluator.cs, tested inDarling.Tests/PostgresAlertEvaluatorTests.cs):- Wraparound risk (
GetWraparoundRiskAsync) - Xmin horizon (
GetXminHorizonAsync) - Replication slot risk (
GetReplicationSlotRiskAsync)
These three read from
DarlingPostgresAlertReadAdapter.csand deliver through the sameIAlertDeliverer/mute-rule/cooldown machinery as every SQL Server alert (per-subject cooldown keying, explicitly documented atDarlingWorker.cs:2797-2799to avoid the [FEATURE] Add a stable, machine‑readable dedup fingerprint (and involved‑object list) to alert payloads - esp. deadlocks & blocking #1140-style collapse-across-subjects bug). This is mature, tested code — it just evaluates conditions (transaction-ID wraparound, replication-slot bloat) that are meaningless for SQL Server, so Postgres is actually AHEAD of SQL Server here, not behind — these three alert types have no SQL Server equivalent at all. - Wraparound risk (
So the real gap, per alert type
| Alert type | SQL Server data | Postgres data | Assessment |
|---|---|---|---|
| Deadlocks Detected/Cleared | deadlocks table |
already collected — pg_deadlocks table (PgDeadlocksCollector), used live tonight via get_pg_deadlocks/get_pg_deadlock_detail |
Straightforward — telemetry exists, nothing evaluates it |
| Blocking Detected/Cleared | dmv_blocking_snapshots/blocked_process_reports |
already collected — PgBlockingCollector/PgLockStatsCollector, used live tonight via get_pg_blocking |
Straightforward — same story |
| Long-Running Query/Cleared | query_snapshots |
plausibly already collected — PgSessionStatesCollector (used live tonight via get_pg_session_states) should carry per-session duration; needs one trace to confirm it's shaped the same way query_snapshots is |
Likely straightforward, needs a quick confirm |
| High CPU/CPU Resolved | cpu_utilization_stats.sqlserver_cpu_utilization |
collected, but a different shape — PgKernelStatsCollector (pg_kernel_stats, used live tonight via get_pg_kernel_stats) reports Aurora/host-level metrics, not a sqlserver_cpu_utilization-shaped column |
Needs light design — what "CPU" alert threshold means differs by source shape |
Poison Wait (RESOURCE_SEMAPHORE_QUERY_COMPILE) |
wait_stats |
no direct analogue — PgWaitStatsCollector/PgWaitSamplingCollector (pg_wait_stats/pg_wait_sampling) capture LWLock/IO/lock waits, but "poison" semantics (which wait type + threshold constitutes a systemic problem) would need its own definition — this specific wait type name is inherently SQL-Server-only |
Needs real design work, not a port |
| Failed Agent Job | running_jobs (SQL Agent) |
not applicable — Aurora/RDS Postgres in this fleet has no SQL Agent equivalent (no pg_cron on managed Aurora) | Confirmed not applicable |
| Wraparound Risk / Xmin Horizon / Replication Slot Risk | n/a | already shipped, tested, engine-gated correctly | Postgres-exclusive, already at parity in the other direction |
| Collector Cost Regression / Store Job Over Cadence / Collection Stopped / Capture Down | both engines | both engines | Already engine-agnostic (DarlingSelfAlertEvaluator, about the tool itself) — already at parity, this is what fires on pgmon tonight |
Where this would plug in
AlertEngine is a single shared class evaluating everything for every server with no engine gate — mirroring it directly for Postgres would mean either (a) adding Postgres-shaped read methods to the SAME IAlertReadAdapter/AlertEngine pair with each check internally branching on data availability, or (b) following the EvaluatePostgresAlertsAsync precedent already established for the three outage predictors — a sibling evaluator gated at the DarlingWorker.cs:2748 call site on CollectorTargetEngine.PostgreSql, reading from DarlingPostgresAlertReadAdapter (or a new adapter), delivering through the same IAlertDeliverer path. Given the codebase's existing engine-seam convention (CollectorTargetEngine/ITargetProvider for collectors, now EvaluatePostgresAlertsAsync for these three alerts), option (b) is the precedent already in place — worth following it rather than inventing new shared-engine branching.
Scope note
This issue is diagnosis/scoping only — no code changes. Deadlocks and Blocking look like the highest-leverage, lowest-risk additions (data already flowing, direct conceptual match). High CPU and Long-Running Query need a small design decision on shape. Poison Wait needs a from-scratch definition of what "poison" means on Postgres, if it's wanted at all.
Activity
Merge pull request #2713 from erikdarlingdata/feature/2711-pg-deadloc…
erikdarlingdata commentedon Aug 31, 2026
Postgres "Poison Wait" analogue — research only, options not a decision
Erik doesn't know what a Postgres equivalent of SQL Server's Poison Wait should even be, so this is candidates grounded in real fleet data, not an implementation.
What makes a SQL Server wait "poison" (confirmed from the code)
DarlingAlertReadAdapter.PoisonWaitsSqlfires on exactly three wait types:THREADPOOL,RESOURCE_SEMAPHORE,RESOURCE_SEMAPHORE_QUERY_COMPILE— worker-thread exhaustion and memory-grant starvation (execution-time and compile-time). All three share one threshold (PoisonWaitThresholdMs, default 500ms avg/wait) with no per-type tuning. That only makes sense because all three share the defining trait: near-zero in healthy operation, so any sustained accumulation is inherently abnormal — unlike CPU or generic I/O waits, which have a real baseline and only become a problem past a tuned threshold.Real data from the pgmon fleet (5 servers, 24h each,
get_pg_wait_stats)Checked
pos-cart-multitenant,pos-cart-tau,pos-cart-zeta,segments-upsilon,segments-multitenant.Baseline, NOT poison-wait material (present on every server, dominant share of total wait time):
IO/XactSync— 83-95% of total wait time on every server checked. This is Aurora's commit-durability wait. Always present under any write load — the Postgres analogue of SQL Server'sWRITELOG, not a poison candidate.LWLock/LockManager— present on all 5 servers, but at trivial volume (5-1,737 waits, sub-millisecond avg). Baseline internal contention, not poison.Candidate 1 —
Lock/Relation(heavyweight relation lock waits). Zero occurrences on 4 of 5 servers checked. On the fifth (segments-multitenant): 114 waits over 24h averaging 7,775.7 ms per wait — 886.4 seconds total, 18.9% of that server's entire 24h wait budget, from just 114 events. Rare-but-catastrophic is exactly the poison shape.EvaluatePgBlockingAsync). A relation-level lock wait IS blocking. Before building a separate "poison" definition here, check whether Add Postgres Deadlocks and Blocking alerts (part of #2711) #2713's blocking alert already catches this case, or whether it's answering a different question (e.g., blocking alerts on an active chain right now vs. this being about accumulated wait time since last collection, which could fire even after the blocker released).Candidate 2 —
IPC/BtreePage+IPC/BufferIo. Exactly zero on 4 of 5 servers. Onsegments-multitenant: 253,971 waits / 538,850 ms (BtreePage, waiting on a B-tree index page another backend holds) and 243,944 waits / 270,493 ms (BufferIo, waiting on another backend's in-flight page read) — 11.5% and 5.8% of that server's total wait time respectively. This is a genuine structural signal distinct from blocking: sustained index-page or buffer contention, not a lock a human query is holding. No overlap with #2713 that I can see. This is the stronger candidate for something SQL-Server's-Poison-Wait-shaped that doesn't already exist elsewhere in the alert catalog.Noted, not enough evidence to propose:
pos-cart-zetahad single one-off occurrences ofSubtransWrite(1,769 ms) andClogExtend(269 ms) — transaction/CLOG subsystem pressure. Rare and expensive fits the pattern, but n=1 each; worth re-checking with a longer window before treating as a candidate rather than noise.What this means for a threshold
Unlike SQL Server's flat 500ms-avg/wait for all three types,
segments-multitenant's two candidates have very different avg-per-wait shapes (Lock/Relation ~7.7s/wait at low volume; IPC/BtreePage ~2.1ms/wait at very high volume, IPC/BufferIo ~1.1ms/wait at very high volume) — a single avg-ms threshold tuned for one will be meaningless for the other. Whatever gets built likely needs either per-event thresholds or a volume-aware trigger (e.g., total accumulated wait time crossing a bar, not avg-per-wait), not a direct port of the SQL Server shape.No implementation attempted here — flagging both candidates, the #2713 overlap question on candidate 1, and the threshold-shape mismatch for whoever picks this up next.
Merge pull request #2722 from erikdarlingdata/feature/2711-pg-long-ru…
Sum poison wait accumulation across wait-name casings (#2711)
Merge pull request #2728 from erikdarlingdata/feat/2711-pg-poison-wait
Carry the #2711 PG wait read on the Poison Wait triage page
erikdarlingdata commentedon Sep 1, 2026
Complete: #2713 (Deadlocks + Blocking), #2722 (Long-Running Query), #2727 (instance CPU collector + High CPU), #2728 (Poison Wait analogue —
IPC:BtreePage/IPC:BufferIoaccumulated-wait bars). Merged todev; ships in the next release. Poison Wait thresholds are documented constants, deliberately re-tunable.Add Postgres Deadlocks and Blocking alerts (part of erikdarlingdata#2711
Add Postgres Long-Running Query alert (part of erikdarlingdata#2711)
Fix erikdarlingdata#2716: none of the 5 Postgres alerts' restart wate…
Add Postgres Poison Wait analogue alert (erikdarlingdata#2711)
SQL Server's Poison Wait alert measures accumulated starvation over a…