Repository navigation
FinOps Server Inventory: one fleet query instead of one per server (#4227) - #4241
Merged
Merged
Conversation
Server Inventory ran GetServerMetricsAsync once per server (43 round trips on a 43-server fleet), each a raw 7-day v_query_stats scan just to answer "did this database run anything". Folds all five per-server CTEs into one fleet-wide statement, and routes the idle-database check through the stitched query_stats_db_hourly/_interval_hourly rollup plus raw only for the two edges the rollup can't cover: the partial leading hour the bucket grain can't slice, and the trailing range the materialized_only aggregate hasn't refreshed yet (read via a new RollupMaterializationWatermark helper, cagg_watermark preferred, max(bucket)+1 bucket as a fallback that only ever reads more raw, never less). Checkpoint: source and unit pins pass; the live boolean-scenario tests, the EXPLAIN ANALYZE before/after table, and the Lite parity check are not done yet (listed in the PR body). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…ases, add live tests The fleet rewrite's idle_dbs CTE grouped an EXCEPT by server_id: a server whose every known database is active has zero rows surviving the EXCEPT, so GROUP BY contributes no row for it at all, and the outer LEFT JOIN then reads idle_db_count as NULL instead of 0. The old per-server statement never had this trap (a bare COUNT(DISTINCT ...) with no GROUP BY always returns one row). Root idle_dbs at servers instead, like size_latest and mem_latest already do, so every server gets a row. Caught by a new live test comparing the fleet statement against the old per-server text server by server. Also adds a live test for the routed idle check across its three regions (stitched rollup, partial leading hour, unmaterialized tail) and the watermark fallback path. Part of #4227. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
3 of 4 tasks
#4227 replaced FinOps Server Inventory's per-server metrics loop with one fleet statement, so it no longer declares a ViewerReadFanOut.Lanes(...) runtime width. That drops NoDeclaredFanOutWidth_OutrunsThePermitsOrTheMeasurement's runtime-width count from two to one; the floor and its doc comment now say why. Part of #4227. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
erikdarlingdata
marked this pull request as ready for review
September 25, 2026 06:14
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.
Part of #4227.
Lane V3a wrote the code and the source/unit pins, then ran out of budget before the live tests. Lane V3e (this pass) ran the live tests, the before/after numbers, the census gates, and the full suite. It found and fixed one real bug the fleet rewrite introduced, and fixed one census pin the change legitimately moves.
Why
FinOps Server Inventory called
GetServerMetricsAsynconce per server (43 round trips on a 43-server fleet). Each call ran a raw 7-dayv_query_statsscan just to answer one boolean per database: did it run anything. Across a fleet that is roughly the whole rawquery_statstable, every 30 seconds the sub-tab was visible.The coordinator's ruling (posted on #4227 and in lane V1's handoff comment) set the shape: one fleet statement instead of 43. The idle check answers from the stitched
query_stats_db_hourlyandquery_stats_db_interval_hourlyrollup plus raw only for the two edges the rollup cannot cover. The rollup routing usesRollupCoverage.StitchedRelationSql, the same routingViewerDataService.FinOps.Workload.csalready uses. That rollup pair ismaterialized_only = true. #4186 froze the legacy relation. It is never read alone.What changes
Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.FinOps.Inventory.cs:ServerMetricsSqlis now one fleet-wide statement (noserver_idparameter) instead of a per-server template.cpu_24handgrantsgroup byserver_id.mem_latest,storage_totalsandlatest_dbsuse a per-serverCROSS JOIN LATERAL ... LIMIT 1descent offservers(the same shapeServerFreshnessSqlalready uses, Fleet overview reads every retained row of four tables on every call, and the web viewer calls it twice a minute per open tab #3895).GetServerMetricsAsync()dropped itsserverIdparameter and now returnsDictionary<int, ServerMetricsRow>for the whole fleet in one round trip.ServerMetricsSqlFor(RollupCoverage, DateTime idleCutoffUtc, DateTime watermarkUtc)swaps the rawactive_dbsCTE for a three-arm union. Arm 1: the stitched rollup for whole hours from the idle cutoff's ceiling hour onward. Arm 2: raw for the partial leading hour[idleCutoff, ceilingHour). Arm 3: raw for the trailing unmaterialized range[watermark, now).idle_dbsoriginally groupedlatest_dbs EXCEPT active_dbsbyserver_id. A server whose every known database is active has zero rows surviving thatEXCEPT.GROUP BYthen contributes no row for it at all, and the outerLEFT JOINreadsidle_db_countasNULLinstead of0. The old per-server statement never had this trap: a bareCOUNT(DISTINCT ...)with noGROUP BYalways returns exactly one row.FinOpsTab.Loaders.cs's overlay only assigns a value that arrived (if (row.IdleDbCount.HasValue)). A healthy, all-active server then keeps showing whatever idle count it last had instead of updating to 0. Fixed by rootingidle_dbsatservers(aLEFT JOIN+COUNT(...) FILTER (...), likesize_latest/mem_latestalready do) so every server gets a row. A live database, not a code read, surfaced it.Darling/PerformanceMonitor.Darling.Storage/TimescaleSupport.cs: newRollupMaterializationWatermarkclass besideRollupCoverage.GetAsyncreads a CAGG's materialization watermark via_timescaledb_functions.cagg_watermark, falling back tomax(bucket) + one bucket widthoff the view itself on any Postgres/cast failure. The fallback is never later than the true watermark, so it only ever reads more raw, never less. V3a confirmed on the rig (as a role carrying only the productionviewergrants) that both the function and_timescaledb_catalog.continuous_aggare callable and readable.Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.Loaders.cs:LoadFinOpsServerInventoryAsyncnow callsGetServerMetricsAsync()once and overlays the returned dictionary in a plain loop. This replaces theViewerReadFanOut.Lanes/Task.WhenAllfan-out that used to run one call per server.Darling/Darling.Tests/ViewerCommandTimeoutTests.cs:NoDeclaredFanOutWidth_OutrunsThePermitsOrTheMeasurementcounted two runtime (Lanes(...).Count-derived) fan-out declarations project-wide, one of which was this loader's now-removed one. Dropped the floor from 2 to 1. This is a deliberate pin update, not a weakening. The other runtime fan-out (MainWindow.xaml.cs's fleet-totals read) is untouched.Darling/Darling.Tests/ServerInventoryFleetMetricsLiveTests.cs(below).Numbers
Seeded probe database: 40 servers, 7 days of hourly CPU/memory-grant/query_stats data, one database_size_stats snapshot per server. Each server had 5 databases, one permanently idle. Rollups refreshed to a 6-hour unmaterialized tail.
EXPLAIN (ANALYZE, BUFFERS), one run each:origin/dev'sServerMetricsSql, summed over 40 runs)ServerMetricsSqlFor, routed through the rollup + raw tail)About 3.6x less total execution time and 20.5x fewer buffer hits, on top of the 40:1 round-trip reduction. Each of the 40 old round trips also paid its own network/connection-pool overhead in production. This in-process measurement does not capture that cost.
Test plan
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: 0 Warning(s), 0 Error(s).Darling.Tests.exe -class "*ViewerFinOpsSqlTests*": 49/49 pass.Darling.Tests.exe -class "*ServerInventoryFleetMetricsTests*": 6/6 pass. One assertion updated to match theidle_dbsfix's SQL shape (LEFT JOIN/FILTERinstead ofEXCEPT).ServerInventoryFleetMetricsLiveTests.cs, both against the rig (TimescaleDB,ScratchPostgres-isolated):ServerMetricsSqlFor_FindsActiveDatabasesInEveryRegion_AndOnlyTheNeverExecutedOneIsIdle: seeds legacy rollup for one hour, successor rollup for the next two, a partial-leading-hour row, and a raw-tail row after the watermark. Over a 5-database universe,idle_db_countreads exactly 1. A miss in any active region (legacy, successor, raw tail, or partial leading hour) pushes the count above 1. Also reads the watermark two ways (cagg_watermarkand themax(bucket)fallback) and confirms both give the sameidle_db_count.GetServerMetricsAsync_FleetResult_MatchesTheOldPerServerStatement_ServerByServer: 3 servers, deliberately different shapes (mixed active/idle, all-active, all-idle with no CPU/memory/grant evidence). Every field (AvgCpuPct,StorageTotalGb,IdleDbCount,ProvisioningStatus) matches the old per-server text (frozen verbatim fromorigin/dev) for all 3 servers. This is the test that caught theidle_db_countNULL-vs-0 bug.McpPayloadContractCensusTests,StartupCommandTimeoutTests,StorageCommandTimeoutTests,AlertReadFailureSurfaceTests,DocCommentHygiene,LivePostgresCollectionHygieneTests,LiveCleanupConversionRatchetTests: 154/154 pass.git merge origin/dev: clean, no conflicts.DARLING_TEST_PG, freshdarlingtest): 13,814 total, 0 errors, 2 failed, 47 skipped, 1 not run.ViewerCommandTimeoutTests.NoDeclaredFanOutWidth_OutrunsThePermitsOrTheMeasurement: the census pin above. Fixed. Re-ran the class alone afterward: 57/57 pass.TrendPayloadBudgetLiveTests.EveryDefaultAnswer_StaysNearTheBudget_AndTheLargestAnswerStaysUnderTheCap: not mine, not confirmed pre-existing. It fails on an MCP trend tool each time ("Exception while reading from stream"), a different tool each run. Nothing in this PR's diff touches trend tools or MCP payloads. It also fails alone on a freshly created database. I did not have budget to check dev's own CI run to call it pre-existing. Reporting it as unrelated and needing a look.CHANGELOG entry
Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ