Repository navigation
Top-N-by-CPU routes to the hourly rollup once raw's floor ages past the window (#4231) - #4396
Merged
Merged
Conversation
…ly rollup (#4231 stage 3a)
- TopQueriesHourlyRoutingLiveTests: proves GetTopQueriesByCpuRoutedAsync routes raw -> hourly once raw's floor ages past a window, through the product's routed read (never the rollup SQL run directly, per the standing raw-vs-rollup gate). - Two seeds: a clean population, and one zero-interval first-collection row admitted by raw but excluded from the hourly rollup by IntervalHonestSourceFilter -- asserted as a disclosed finding, not smoothed over. - RED on dev (no GetTopQueriesByCpuRoutedAsync exists there; the plain GetTopQueriesByCpuAsync stays raw-only and returns empty once raw is purged for the window).
…ting live pins Both live tests in TopQueriesHourlyRoutingLiveTests.cs were failing in-process because the tests, not the router, were wrong: 1. Both tests reused ONE NpgsqlDataSource for the pre-purge (raw) and post-purge (hourly) routed calls. ComposeStoreAvailability caches coverage per data-source instance for a 5-minute TTL, so the second call replayed the first call's null hourly floor (measured before RefreshAsync materialized the successor), and the router's coverage-degrade path never resolved to Hourly. Fixed by opening a fresh NpgsqlDataSource for the post-refresh call in both tests. 2. The DOP/rollup precision_note test's PlantAsync seed left max_dop NULL, which fails the min_dop=2 filter's HAVING COALESCE(MAX(max_dop), 0) >= $6 floor, so the raw call returned zero rows (the 'empty' payload, no tier_used field) instead of exercising the pin. Fixed by giving PlantAsync an optional maxDop parameter and seeding this test's rows with maxDop: 2. No product code changed. RED confirmed on dev (bb04415): compile failure, GetTopQueriesByCpuRoutedAsync does not exist there.
erikdarlingdata
marked this pull request as ready for review
September 26, 2026 11:59
This was referenced Sep 26, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 26, 2026
…urly rollup (#4413) Top-N procedures by CPU route to the hourly rollup once raw retention has dropped the window, following #4396 for queries. - DarlingDataReader.TopProceduresHourlySql reads the hourly procedure rollup. Its FROM clause is substituted only through RollupCoverage.StitchedRelationSql. - GetTopProceduresByCpuRoutedAsync resolves the tier with RetentionTierRouter. A daily verdict is clamped to hourly. - The MCP payload gains tier_used and precision_note. The raw-floor probe and the window_truncated/effective_start disclosure run only on the raw tier. The WPF Viewer mirrors the routing with GetTopProceduresByCpuTierAsync and marks the grid header when the result is aggregated hourly. - Tests: TopProceduresHourlyRoutingTests (SQL shape) and the live TopProceduresHourlyRoutingLiveTests. At 7 days and 10,081 rows, the hourly read took 2.3-3.1 ms and 156 buffers, against raw's 2.8 ms and 206. Refs #4231
This was referenced Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #4231.
Why
get_top_queries_by_cpu(and its Storage/Viewer counterparts) only ever read rawquery_stats. Once retention drops raw's chunks for an older window, the read returns nothing for that window even though the hourly continuous aggregate still holds a summed answer — the same class of silent-empty-window bug #1937/#1759 fixed for other readers. This wires the top-N-by-CPU family into the standingRetentionTierRouter+RollupCoveragegate so it degrades to the hourly rollup instead of going empty, and discloses the precision loss (tier_used,precision_note) rather than hiding it.What changes
DarlingDataReader.TopQueriesHourlySql(new const): the hourly-tier twin ofTopQueriesSql, groups by(database_name, query_hash)only (the rollup carries nohost_object_name), ranks bySUM(worker_time_sum) DESC. FROM clause is a$FROM$placeholder, substituted only viaRollupCoverage.StitchedRelationSql— never a literal relation name (the standing gate).GetTopQueriesByCpuAsyncnow delegates toGetTopQueriesByCpuRoutedAsync, which resolves the tier viaRetentionTierRouter.ResolveoverRollupCoverage.For(QueryStatsHourlyView, QueryStatsDailyView). ADailyverdict is clamped toHourly(daily top-N is out of scope here).TopQueriesHourlyTextLookupSql: one follow-up query per ranked hourly row, resolving a representativequery_textfromv_query_stats(the rollup has none).DarlingMcpDataTools.GetTopQueriesByCpuaddstier_used("raw"/"hourly") andprecision_note(set only when hourly) to the MCP payload; skips the raw-floor probe entirely on the hourly path (an hourly-routed read never touchedquery_stats).ViewerDataService.GetTopQueriesByCpuTierAsyncmirrors the same routing for the WPF Viewer;ViewerServerTab.Queries.csappends an "— aggregated hourly, per-caller detail unavailable" note to the truncation banner when the tier is hourly.TopQueriesHourlyRoutingTests.cs(source-level, no live store): the SQL shape and the never-names-the-relation-directly source pin.TopQueriesHourlyRoutingLiveTests.cs(new) — the live equality/routing pin described below.Test plan
TopQueriesHourlyRoutingTests.cs(source-level): both green in-process on this rig.TopQueriesHourlyRoutingLiveTests.cs(live, own scratch database viaScratchPostgres/#1776 own-store):dotnet Darling.Tests.dll -class Darling.Tests.TopQueriesHourlyRoutingLiveTests, WindowsDesktop entry stripped, ownpmw-4231-3a-debugTimescaleDB 2.30.1 container, removed after). Both tests in the file now pass:Total: 2, Errors: 0, Failed: 0.ComposeStoreAvailabilitycache, not the router itself.RetentionTierRouter.Resolveis correct as designed — the test's premise (a fixed calendarWindowStart, realDateTime.UtcNow, delete the window's raw rows so it ages pastRawMaxAge, 3 days) is sound. The actual cause:ComposeStoreAvailability's answer cache is keyed perNpgsqlDataSourceinstance, with an unconditional 5-minuteReprobeIntervalTTL. Both tests reused ONENpgsqlDataSourcefor both the pre-purge (raw) and post-purge (hourly) routed calls. Call 1's probe ran and cachedTierCoveragewith a null hourly floor — measured beforeRefreshAsyncmaterialized the successor over the window — and that cached answer is still fresh (well inside 5 minutes) when call 2 runs milliseconds later on the SAME data source. The router's coverage-degrade path (RetentionTierRouter.Resolve(... TierCoverage)) sees the age-picked Hourly tier failTierCoverage.Coversagainst the stale null floor and falls back toward Raw. Fix: both tests now open a FRESHNpgsqlDataSourcefor the post-refresh/post-purge (hourly) call, so its coverage probe measures the store's true post-refresh state. This is a genuine cache-freshness gap inComposeStoreAvailability— a live production store's routed reads could see the same up-to-5-minutes-stale answer right after a backfill or CAGG refresh finishes. Not fixed in the composer itself here — recorded as a finding; the fix belongs inComposeStoreAvailability's own single-flight/TTL design, not in a live test.GetTopQueriesByCpu_HourlyRouted_WithDopOrRollUp_CarriesPrecisionNote) still failed after the cache fix, onAssert.True(rawDoc.RootElement.TryGetProperty("tier_used", ...)). Cause:PlantAsync's seed rows never setmax_dop(left NULL); the raw call'smin_dop=2filter isHAVING COALESCE(MAX(max_dop), 0) >= $6, so a NULLmax_dopfails the floor, the raw call's population filters to zero rows, and the tool returns the"empty"status payload — which carries notier_used/precision_notefield at all, hence theTryGetPropertyfailure. Fixed by givingPlantAsyncan optionalmaxDopparameter and seeding this test's two rows withmaxDop: 2, matching the pin's own stated intent (an hourly-routed payload "withmin_dopset"). Both tests in the file are green after this fix.git worktree add --detach /tmp/pmw-4231-3a-debug-red bb04415cd(dev's tip at merge-base with this branch), copiedTopQueriesHourlyRoutingLiveTests.csin, builtDarling.Tests.csprojthere — compile failure:CS0117: 'DarlingDataReader' does not contain a definition for 'GetTopQueriesByCpuRoutedAsync'at both call sites — the routed method doesn't exist on dev. Worktree removed after.net10.0-windowsand cannot run natively on macOS; the in-process run above uses the WindowsDesktop-stripped runtimeconfig workaround, which is not CI's real execution path. CI runs it for real on Windows.Darling.Testsbuilds 0 errors / 0 warnings on this branch (-p:EnableWindowsTargeting=true).Known limits
rollUpByHostObjecton the hourly path: an hourly-routed read silently ignoresminMaxDopandrollUpByHostObject— the rollup has no per-group DOP column and nohost_object_nameto roll up by. Disclosed viaprecision_note, not erroring.GetQueryStatsWindowFloorAsync) only runs for the raw tier — an hourly-routed read never touchesquery_stats, sowindow_truncated=false/effective_start=requestedStartis reported unconditionally there rather than re-probing a table the read never opened.TopQueriesSqlcounts asample_interval_seconds = 0first-collection row; the hourly rollup's CREATE bakes inIntervalHonestSourceFilter(sample_interval_seconds IS DISTINCT FROM 0) and excludes it. On the live pin's seed this is a 900,000 CPU-us / 1-execution difference for that one query_hash — invisible unless that hash happens to rank in the top-N, since every OTHER group's raw and hourly totals agree exactly. Raw's own read is unchanged here; whether raw's read should apply the same filter is a separate design question.Measurement
Own seeded rig: 20 servers × 7 days × 96 fifteen-minute cycles × 5 query groups (67,200 raw rows), one bulk
INSERT ... generate_series, on a fresh TimescaleDB 2.30.1 container (ownpmw-4231-3a-pinscontainer, removed after the run). Reads are for one sample server's full 7-day window, top 20.EXPLAIN (ANALYZE, BUFFERS)— ranking query, Execution TimeEXPLAIN (ANALYZE, BUFFERS)— largest single Buffers lineAt this seed's scale (well short of the issue's field population of 4,078 ms / 162.9k buffers at 7 days) both tiers already answer in single-digit milliseconds on a local container, so the wall-clock gap is not yet the dominant signal — the buffer-count gap is: the hourly rollup's ranking query touches roughly 30x fewer shared buffers than raw's equivalent scan (285 vs 8375 largest line), which is the shape that would separate BEFORE and AFTER by seconds at the field's row counts. The N text lookups (one per ranked row, 5 rows here) added no measurable wall time at this scale — well under a 10% threshold — so the N+1 collapse described below was not triggered; a store with a much wider top-N or a slower text-dimension table could still see it matter, and is left as-is (collapse it only if it costs more than about 10%).
CHANGELOG entry
SECTION: Changed
ENTRY:
get_top_queries_by_cpu(MCP tool, Storage reader, and Viewer) degrades to the hourly continuous aggregate for a window older than raw's floor and discloses the precision loss (tier_used,precision_note) rather than silently returning nothing.REF:
[Top-N-by-CPU routes to the hourly rollup once raw's floor ages past the window (#4231) #4396]: Top-N-by-CPU routes to the hourly rollup once raw's floor ages past the window (#4231) #4396