Repository navigation
Read the interval table below the raw floor, down to the exact bound it can prove, and say how far back it reaches (#4689) - #4700
Merged
Conversation
… a provably exact bound
…elow-floor live pins
…uery Store panels (#4689)
… of failing the gate to raw
…route-only floor probe and history_source
…et Compose's history start (#4689)
…ster; pin purge-edge and route exactness (#4689)
… the range edges as boundary rows (#4689)
… day and cadence (#4700)
…table clamp, count SUCCESS rows only, keep the at-floor read when the cadence probe fails (#4700)
erikdarlingdata
marked this pull request as ready for review
September 29, 2026 02:19
This was referenced Sep 29, 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.
Fixes #4689.
Summary
query_store_interval_widereads were clamped at raw's chunk floor, so a window longer than the raw tier lost its older part even though the table still held it. Reads now go below the floor down toL = max(window start, filled_since, table floor + 1 day)and no further. Each surface reports the start it actually served.GetQueryStoreTop(MCP tool) now returnshistory_source(raworinterval_table) andeffective_start, and it builds atruncation_notethat names which bound set the start. The reason sentences are:FilledSince: "The interval table began keeping complete history for this server at {effective_start}."TablePurgeEdge: "The interval table keeps 9 days, and intervals that began before its purge edge are not read."RawFloor/Window: "The read is clamped at the raw tier's retention floor: nothing older than it can be shown exactly."A
WideStartBoundenum names the bound (ties preferFilledSince, thenTablePurgeEdge, thenWindow).ExactBelowFloorStartcomputes the start andResolveReadAsyncreturns it asReadStart.Fixed here:
ResolveReadAsyncbound Kind=Utc DateTimes totimestampparameters. Npgsql refuses that, the gate's catch fell back to raw, and the MCP tool (which resolves its window fromDateTime.UtcNow) never read the table on the unreleased #4341 route; each such call logged a warning and read raw. The gate now normalizes its window arguments to Unspecified.Hazards (why L is what it is)
filled_sinceare outside the coverage claim.first_execution_time, so the oldest intervals can be partly purged (hence table floor + 1 day).interval_start_time_utc IS NULL) differ from raw.filled_since(covered by H1).Design notes
first_execution_time.QueryStoreCollector.cs:805).Lite/finds noquery_store_interval_wide).PlainTableFloorSqlisMIN(first_execution_time) WHERE server_idon that index. It is cheap when the server has old rows and a full index walk for a server with coverage but no rows.DarlingRetention.cs(9-day constant and the purge comment) and theQueryStoreIntervalWide.cssection comment,ClampedStartdoc (the clamp is the bound at and above raw's floor;ResolveReadAsyncextends it below).QueryStoreIntervalWide.HistoryNote/BannerReason), pinned to occur once.logger: null. A gate fault falls back to raw silently, which is why the Kind defect above left no trace in the service log. Wiring a logger through those static readers is a separate change.Pins and REDs
Live class
QueryStoreIntervalWideBelowFloorLiveTests(7 tests, local TimescaleDB rig, 7/7 green). Each mutation was a temporary code change, reverted after:ExactBelowFloorStartignoringfilledSince: RED, 3 fail (FilledSinceBound_IsLoadBearingline 138/150,Boundary_IsInclusive,Disclosure_EffectiveStart…).tableFloor + IntervalSpanMargin: RED, 2 fail (TablePurgeEdge_Boundline 215/220,Disclosure_EffectiveStart…).>instead of>=oncollection_timein the MCP table read: RED, 2 fail (Boundary_IsInclusiveline 193/196,PlainStore_ReadStartIsWindowStartline 234).Unit:
QueryStoreIntervalWideGateTests23/23. Also addedhistory_sourceto theDarlingMcpDataToolsTestspayload pin (raw), and aQueryStoreTopWindowTestspin that the raw floor probe sits after the interval-table branch.Census and neighbours run on the rig (745 tests,
QueryStoreTop*,QueryStoreIntervalWide*,RawWindowFloor*,DarlingMcpDataToolsTests-adjacent pins,McpToolsListBudgetTests,McpReadCommandTimeoutTests, every*Census*,*AdoptionTests, timeout/deadline classes,DocCommentHygieneTests): 741 pass, 4 fail. The 4 areRawWindowFloorViewerPortTests.UpdateTruncationBanner_*, which need WPF (STA) and cannot run on macOS. Darling.Tests and Viewer builds: 0 warnings, 0 errors.DarlingMcpDataToolsTestsitself was not run as a class (Windows-only DPAPI cases); its one edit is a one-line assertion.QueryStoreIntervalWideHistoryNoteTests(14): exactHistoryNotestrings for every bound and both scopes, exactBannerReason, the Compose forwarder, and a census that the purge-edge phrase occurs once in product source (inQueryStoreIntervalWide.cs). RED:HistoryNotesaying "this server" whenmanyServersis true fails 4 of 14; restored, 14/14.DarlingMcpDataToolsSurfaceAndSqlTests64/64.Rig EXPLAIN (local TimescaleDB rig, not production numbers)
MCP table read (
QueryStoreTopTableSql) at a below-floor start, 2,000,000_widerows over 40 servers, one server, 6.5-day window:Index Scan using ux_query_store_interval_widereturned 37,036 rows;Buffers: shared hit=50835for the scan, 50,847 total; small sort spill (temp read 482); Execution Time 72 ms.Viewer grid and Compose
Queries grid. The Query Store grid now calls
ResolveReadAsyncand binds the plan'sReadStart(newGetQueryStoreTopQueriesWithReachAsyncreturns rows plus the plan; the old method delegates). When the interval table served, the banner readsShowing since <EffectiveStart>plus(interval table complete from then)or(interval table keeps 9 days), and appends· slicer since <raw floor>when raw's floor is truncated (the slicer stays raw). The raw route's banner is unchanged.Duration trend. No behaviour change: a doc line at its
ReadsTableAsynccall says it stays on the clamp, because_widecan hold legacy NULLinterval_start_time_utcrows that make the table differ from raw, and below raw's floor they cannot be checked without a probe that walks every server's rows.Compose. The #4617 Compose path read the interval table below the raw floor unclamped and was not exact there; this binds the exact start.
ResolveQueryStoreWideEligibleAsyncnow callsResolveReadAsyncper server and returns the latestReadStartas one commonWideStart(plus the bound of the server that set it).ComposeRunContext.QueryStoreWideStartcarries it; the compiler binds it as its own parameter on the table read'scollection_timepredicate (collection_timeisquery_store_stats' time column). The response addsquery_store_history_startsandquery_store_history_notewhen the table served from a start later than the window's.Pins.
DarlingComposeTests(SQL carries the wide-start bound when set, not when null/earlier; note text),RawWindowFloorViewerPortTests(banner branches, wiring and trend doc-line source pins; the banner test needs WPF and cannot run on macOS), and liveComposeQueryStoreWideStartLiveTests(filled_since forced to S+12h, backdated table rows at S+1h excluded). RED: binding no wide-start parameter makes the live test fail (earliest bucket is below filled_since); restored and green.The purge-edge margin and the cadence gate
query_storecadence to be 60 minutes or less (the per-server override, else the fleet override, else the 5-minute default, fromconfig.config_collector_schedules) AND the largest gap between successful (SUCCESS)query_storecollection-log rows over [the table floor's UTC day, that day + 3 days] to be 60 minutes or less, with the two range edges counted as boundary rows so a lead-in or tail gap counts. No rows in that range counts as unknown and stays clamped. Otherwise the read is clamped at raw's floor with the reasonRawFloorSlowCadence, and the banner says "slow Query Store cadence".frequency_minutes >= 0, which is why the gate exists.collection_logis kept 60 days, so a 9-day-old edge is always covered. On a local rig (20 servers x 2 collectors x 30 days at 5 minutes) the gap read is an index scan onidx_collection_log_watermarkwith the server, collector and time range in the index condition: 576 rows, shared hit=216 read=6, 0.52 ms.query_store_history_set_by.The collection-gap verdict cache
collection_logchunks; a 43-server Compose panel paid about 2.7 s per render. The verdict is now cached.query_storecadence). The window is [floor day, floor day + 3 days], derived from the key only: a superset of [floor, floor + 2 days] for every floor in that day, so it can only clamp more. One entry per (store, server); storing a new floor day or cadence drops that server's older entry. A null or failed read is not cached, and a restart starts empty. The cadence is resolved on every decision.collect.collection_logisDarlingObservability.InsertCollectionLogSql(callers atDarlingObservability.cslines 264, 397 and 514), which writescollection_time = DateTime.UtcNow. No import, backfill or replay path inserts into it, so a settled window's answer cannot change. The gap read countsSUCCESSrows only (SKIPPEDis written only by Lite, which has no interval table).SUCCESSonly): median 38.9 ms and 700 buffers per server (max 45.3 ms, 728 buffers), 1,638 ms summed; the earlier 2-day read measured about 65 ms and 480 buffers per server on colder chunks. So a 43-server Compose panel pays about 1.6-2.7 s on its first render after each floor-day rollover per service process, then cache hits. Largest collection gaps on that store were 13.1-24.7 minutes, so none of its 43 servers is clamped by the gate.QueryStoreIntervalWideGapCacheLiveTests): a second decision issues no read; a cadence change recomputes; a moved floor day recomputes (a day-D verdict never serves D+1); a window still filling is not cached; a gap past floor + 2 days now clamps (the conservative trade of the day-wide window). REDs: a key without the cadence, never caching, and no settle rule each fail a pin. The below-floor, gap-cache and Viewer floor classes share one serial xunit collection because they share the cache's clock and read counter.CHANGELOG
SECTION: Changed
ENTRY: - Query Store views over windows longer than the raw tier now show the full window from the interval table, and say how far back it reaches ([#4700]) - The Queries grid, the get_query_store_top MCP tool and Compose Query Store panels read the per-interval table below the raw tier's floor, down to the exact point where it provably holds the same rows raw held. The MCP tool reports history_source and effective_start, and its note says which bound set the start.
REF: [#4700]: #4700