Skip to content

Postgres fleet (pgmon) has no per-target operational alerting — only tool-health and 3 outage-predictor alerts fire #2711

Closed

Description

@erikdarlingdata

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:

  1. await engine.EvaluateServerAsync(snapshot, cancellationToken) — the shared AlertEngine, 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_stats selecting sqlserver_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 via TargetTable => on PgDeadlocksCollector.cs:102, PgKernelStatsCollector.cs:108, PgWaitStatsCollector.cs:125). So for a Postgres server_id, every one of these queries returns zero rows — there is no explicit engine gate in AlertEngine/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.

  2. 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" via PostgresAlertEvaluator.Evaluate (PerformanceMonitor.Alerting/PostgresAlertEvaluator.cs, tested in Darling.Tests/PostgresAlertEvaluatorTests.cs):

    • Wraparound risk (GetWraparoundRiskAsync)
    • Xmin horizon (GetXminHorizonAsync)
    • Replication slot risk (GetReplicationSlotRiskAsync)

    These three read from DarlingPostgresAlertReadAdapter.cs and deliver through the same IAlertDeliverer/mute-rule/cooldown machinery as every SQL Server alert (per-subject cooldown keying, explicitly documented at DarlingWorker.cs:2797-2799 to 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.

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

  1. added a commit that references this issue on Aug 31, 2026
  2. erikdarlingdata commented on Aug 31, 2026

    @erikdarlingdata
    OwnerAuthor

    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.PoisonWaitsSql fires 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's WRITELOG, 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.

    Candidate 2 — IPC/BtreePage + IPC/BufferIo. Exactly zero on 4 of 5 servers. On segments-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-zeta had single one-off occurrences of SubtransWrite (1,769 ms) and ClogExtend (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.

  3. added 4 commits that reference this issue on Aug 31, 2026
  4. erikdarlingdata commented on Sep 1, 2026

    @erikdarlingdata
    OwnerAuthor

    Complete: #2713 (Deadlocks + Blocking), #2722 (Long-Running Query), #2727 (instance CPU collector + High CPU), #2728 (Poison Wait analogue — IPC:BtreePage/IPC:BufferIo accumulated-wait bars). Merged to dev; ships in the next release. Poison Wait thresholds are documented constants, deliberately re-tunable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions