You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The plan-regression analysis read cannot complete on a large store, and its timeout is swallowed silently #2810
PgFactCollector.CollectPlanRegressionFactsAsync (PerformanceMonitor.Darling.Analysis/PgFactCollector.QueryPerf.cs:409) runs PlanRegressionSql with no CommandTimeout, so it inherits Npgsql's undocumented 30 s default. On the use1 monitoring host the query cannot finish in 300 s, so it is cancelled on every single analysis pass and has almost certainly never produced a fact on that store.
Running the shipped query string (extracted from the assembly source, not retyped), against the busiest server_id, with production-shaped parameters:
PREPARE pr(int, timestamp, timestamp) AS <PlanRegressionSql>;
EXPLAIN (ANALYZE, BUFFERS, TIMING, SUMMARY)
EXECUTE pr(1671144557, now() - interval '15 days', now() - interval '16 days');
SET statement_timeout = '300s';
Time: 300303.379 ms (05:00.303) <- hit the ceiling, did not complete
This is the dominant remaining store cancellation
Grouping every canceling statement due to user request in the store's own PostgreSQL log since the 2026-09-02 22:54:56Z restart:
81 x WITH deduped AS ( -- Collapse incremental re-collections of the same open runtime-stats interval ...
12 x SELECT DISTINCT database_name FROM query_store_stats WHERE server_id = $1 AND collection_time > $2 ORDER BY database_name
---
93 total in ~1 hour
The first is PlanRegressionSql. The second is QueryStoreBackfill.cs:404.
Note for anyone re-running this analysis: an earlier pass reported ~1,673 of these as an unattributed "blank STATEMENT" bucket. That was a parser artifact, not a mystery — PostgreSQL writes STATEMENT: and then the SQL on following lines for a multi-line statement, so a parser reading only the remainder of the STATEMENT: line sees an empty string. There is no unattributed bucket.
The failure is invisible
catch(Exceptionex)when(!AnalysisShutdown.IsExpectedAbandon(ex,context.CancellationToken)){/* Table may not exist or have no data. An abandonment is NOT swallowed here (#2443). */}
A timeout is neither "table missing" nor an abandonment, but it lands here and is discarded. Nothing reaches collection_log, nothing reaches the service log. The only trace anywhere is a cancellation line in the store's own PostgreSQL log — the same blind spot as #2795.
The consequence is a false negative in product output: "no PLAN_REGRESSION findings" and "the plan-regression read failed" are indistinguishable to anyone reading an analysis.
Why raising the timeout is NOT the fix
At >300 s against a 30 s default, no plausible CommandTimeout makes this succeed, and a larger one would only burn more store CPU before failing. The query is slow because query_store_stats holds 19 days / 65 GB at a 4-day policy — see the retention-paused issue filed alongside this. On a correctly retained store the query's own 15-day chunk-exclusion bound (#2387) would prune to ~5 chunks; here it prunes 3 of 19.
So the ordering is: fix retention first, then re-measure this query before deciding whether it needs any change at all.
What still wants doing regardless of retention
The silent swallow. A timed-out fact read should say so, naming the consequence. Doing it properly needs an ILogger on PgFactCollector, which today takes only an NpgsqlDataSource — a ctor change across 7 call sites, worth its own PR.
Scope of the missing-timeout pattern is larger than #2795 recorded
#2795's "Not covered here" cites three files (18/13/9 commands). Counted across all non-test, non-deprecated Darling production sources:
35 files, 160 `new NpgsqlCommand` sites with no CommandTimeout anywhere in the file
Including PgAnomalyDetector (12), PgFindingStore (8), all six PgFactCollector.* partials (31), DarlingSelfAlertEvaluator (6), PgMuteRuleStore (6), DarlingObservability (6).
For contrast, the paths that do set it: DarlingCollectorRunner 9/9, PgDrillDownCollector.Queries 8 commands / 9 timeouts (DrillDownCommandTimeoutSeconds = 30), TimescaleSupport 32/26. The convention exists; the analysis, alerting and config paths never adopted it.
Evidence gathered read-only (default_transaction_read_only = on) via psql on the box.
Fixed by #2869, merged to `dev` as `df03e50d`. Closing manually — `Fixes #N` only fires on the dev→main release merge.
The diagnosis changed, exactly as this issue predicted it might. The original conclusion — "raising the timeout is NOT the fix" — was correct at >300s, and the issue said what to do about that: "fix retention first, then re-measure this query before deciding whether it needs any change at all." Retention was fixed (#2809), and the re-measurement gives a different answer.
Measured on the production store now, shipped query string, six busiest servers, two passes:
rows in window
cold
warm
4,561,927
34,721 ms
21,245 ms
4,252,879
28,181 ms
13,706 ms
3,614,035
32,404 ms
10,589 ms
3,563,352
14,406 ms
10,568 ms
3,370,902
10,985 ms
10,790 ms
3,369,166
10,612 ms
10,591 ms
Two servers cross 30 s cold, a third sits at 28.2 s, and every one is well under it warm. So the read fails on the cold/large combination only — which is why it looked intermittent, and why measuring it by hand made it look fine (the second run is warm).
That also explains why #2827 and #2826 did not fix it: #2827 made the query 2.4x faster and #2826 made the failure visible, but the binding constraint was a 30 s ceiling neither touched — and nobody had ever chosen it. Item 2 of this issue ("explicit CommandTimeout, so the value is a deliberate documented choice") turned out to be the whole fix.
Set to 60 s, bounded on both sides: above the 34.7 s measured cold worst case, and at half the 120 s analysis pass budget that thirty collect methods share — a deadline near 120 s would let one stalled read consume the pass and cost the server its other twenty-nine facts.
Applied to all 31 PgFactCollector commands rather than the one that exposed it, and pinned structurally. The remaining ~129 sites across the other 34 files this issue counted are still open — PgBaselineProvider is the live one, its io_latency baseline currently timing out for what is very likely the same reason.
What
PgFactCollector.CollectPlanRegressionFactsAsync(PerformanceMonitor.Darling.Analysis/PgFactCollector.QueryPerf.cs:409) runsPlanRegressionSqlwith noCommandTimeout, so it inherits Npgsql's undocumented 30 s default. On the use1 monitoring host the query cannot finish in 300 s, so it is cancelled on every single analysis pass and has almost certainly never produced a fact on that store.Running the shipped query string (extracted from the assembly source, not retyped), against the busiest
server_id, with production-shaped parameters:This is the dominant remaining store cancellation
Grouping every
canceling statement due to user requestin the store's own PostgreSQL log since the 2026-09-02 22:54:56Z restart:The first is
PlanRegressionSql. The second isQueryStoreBackfill.cs:404.Note for anyone re-running this analysis: an earlier pass reported ~1,673 of these as an unattributed "blank STATEMENT" bucket. That was a parser artifact, not a mystery — PostgreSQL writes
STATEMENT:and then the SQL on following lines for a multi-line statement, so a parser reading only the remainder of theSTATEMENT:line sees an empty string. There is no unattributed bucket.The failure is invisible
A timeout is neither "table missing" nor an abandonment, but it lands here and is discarded. Nothing reaches
collection_log, nothing reaches the service log. The only trace anywhere is a cancellation line in the store's own PostgreSQL log — the same blind spot as #2795.The consequence is a false negative in product output: "no PLAN_REGRESSION findings" and "the plan-regression read failed" are indistinguishable to anyone reading an analysis.
Why raising the timeout is NOT the fix
At >300 s against a 30 s default, no plausible
CommandTimeoutmakes this succeed, and a larger one would only burn more store CPU before failing. The query is slow becausequery_store_statsholds 19 days / 65 GB at a 4-day policy — see the retention-paused issue filed alongside this. On a correctly retained store the query's own 15-day chunk-exclusion bound (#2387) would prune to ~5 chunks; here it prunes 3 of 19.So the ordering is: fix retention first, then re-measure this query before deciding whether it needs any change at all.
What still wants doing regardless of retention
ILoggeronPgFactCollector, which today takes only anNpgsqlDataSource— a ctor change across 7 call sites, worth its own PR.CommandTimeout. Not to stop the cancellations, but so the value is a deliberate, documented choice rather than an Npgsql default nobody wrote down. This is the Server-scoped watermark read is unbounded, times out, and silently degrades every collector to its fallback window #2795 lesson applied.Scope of the missing-timeout pattern is larger than #2795 recorded
#2795's "Not covered here" cites three files (18/13/9 commands). Counted across all non-test, non-deprecated Darling production sources:
Including
PgAnomalyDetector(12),PgFindingStore(8), all sixPgFactCollector.*partials (31),DarlingSelfAlertEvaluator(6),PgMuteRuleStore(6),DarlingObservability(6).For contrast, the paths that do set it:
DarlingCollectorRunner9/9,PgDrillDownCollector.Queries8 commands / 9 timeouts (DrillDownCommandTimeoutSeconds = 30),TimescaleSupport32/26. The convention exists; the analysis, alerting and config paths never adopted it.Evidence gathered read-only (
default_transaction_read_only = on) via psql on the box.