Repository navigation
Bound the Query Store backfill candidate read at its horizon (#4197) - #4306
Merged
Merged
Conversation
…4197) The candidate DISTINCT and the per-database MIN(last_execution_time) scan both bound at CandidateWindow (7 days) or nothing at all, wider than raw retention, so they decompressed every retained chunk on every tick (206 ms / 21,411 buffers measured on a rig's heavy server). Bind both at floorLimit (HorizonFor) instead, the same boundary the loop already uses to decide whether a database needs digging, and union the candidate list with every database a hole key names so a database that goes fully quiet mid-hole is still serviced instead of silently stranded. Mirrors the fix in Lite's twin backfill worker so the two SKUs keep one rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Covers the four DARLING items from #4306's handoff plan: the candidate read servicing a quiet hole while dropping a finished database and finding a new one, GetStoredFloorAsync's two floor facts (an at-or-before-floor row returns the floor itself, a backfilled row returns its true bounded MIN), a plan-shape pin that the bounded candidate read excludes the compressed old chunk entirely, and an EXPLAIN (ANALYZE, BUFFERS) of the EXISTS miss's worst case (a new database with nothing older than the floor, which has to walk the compressed chunk before concluding no match). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…bound Part of #4197 (lane A, Lite tests). GetBackfillCandidateDatabasesAsync and GetMinCollectedTimeForDatabaseAsync were private, unlike Darling's internal twins, so nothing outside the class could assert on them. Flip both to internal (visibility only, no logic change) and add a DuckDB fixture test covering the same three database cases and two floor facts as the Darling live test: - the candidate read keeps a quiet database with an open hole, drops a finished database with no recent rows, and finds a new database; - a database with a row at or before floorLimit gets floorLimit back; a database without one gets the true MIN(last_execution_time) from a backfilled row (old last_execution_time, recent collection_time). Verified both tests fail against dev's pre-fix shape (no floorLimit/state params, no hole merge, unconditional MIN) by temporarily reverting the bound/merge in a local build, then restored the fix and confirmed green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
GetStoredFloorAsync now builds two commands (an EXISTS pre-check and a MIN fallback) instead of one, which bumped the command-plane site census from 9 to 10; ExpectedSiteCount and the doc comments describing it were not updated. QueryStoreBackfillTests.cs also carried its own private ReadRepoFile copy instead of the shared RepoFile.ReadRepoFile, tripping the "exactly one declarer" adoption census. Both are mechanical: bump the constant, and call the shared reader instead of a local duplicate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…d' into fix/4197-backfill-candidate-bound
DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks failed CI on CommandPlaneCommandTimeoutTests.cs:110: the #4197 note was a second <summary> stacked on the original. It is now one summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
erikdarlingdata
marked this pull request as ready for review
September 25, 2026 16:52
erikdarlingdata
enabled auto-merge (squash)
September 25, 2026 16:52
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 #4197.
Why
Lane D3's design (issue comment
5830310090) and the coordinator's ruling (5834871359) measuredthat the Query Store backfill's candidate read (
SELECT DISTINCT database_name ... collection_time > now - 7 days) and its per-databaseMIN(last_execution_time)floor lookup both bind at a windowwider than raw retention. Neither predicate excludes any chunk as a result. On the design lane's rig,
the heavy-server candidate read cost 206 ms and 21,411 buffers. Bounded at the backfill's own floor
(
floorLimit, fromHorizonFor), the design measured 1.7 ms and 792 buffers. This lane implementsthe ruling's decisions 1-4 (part a) for both Darling and Lite. Part b (persisting ring-buffer
watermarks) is explicitly out of this lane and waits for a field measurement after #4209.
What changes
QueryStoreBackfill.RunServerSliceAsync(Darling) now computesfloorLimit(HorizonFor)before calling
GetCandidateDatabasesAsync, not after. It bindsfloorLimitas the candidateread's own
collection_timelower bound in place of the old 7-dayCandidateWindow(removed).GetCandidateDatabasesAsyncunions the store's bounded result with every database a hole keyalready names. It does this through a new shared helper,
QueryStoreBackfillState.MergeHoleDatabases(sorted ordinal, deduped). A database that goes fully quiet mid-hole (no row newer than the horizon)
is still serviced this way, instead of silently aging out of the scan the moment its recent rows
expire.
GetStoredFloorAsynctakes its exact bounded form:EXISTS ... collection_time <= floorLimit LIMIT 1first. A hit means the database is done (see the method's doc comment for why that isexact:
last_execution_timenever exceeds its own row'scollection_time). Otherwise it fallsback to
MIN(last_execution_time)bounded atcollection_time > floorLimit, which ranges over theexact same rows the old unbounded MIN did once the EXISTS branch is ruled out.
RemoteCollectorService.QueryStoreBackfill.cs) gets the identical shape: the samebound, the same hole-key merge, the same exact floor form, using the shared
QueryStoreBackfillState.MergeHoleDatabaseshelper. Lite persists nothing (ruling item 3).the inline comment, and
ServiceCommandDeadlines.QueryStoreBackfillReadSeconds's doc comment, whichalso claimed the MIN "cannot be bounded." Both now say what is true after this change. The 120 s
timeout value itself is unchanged: it is now a wide safety margin rather than a close one.
CandidateWindow(the unused 7-day constant) is removed.Test plan
Darling/Darling.Tests/Darling.Tests.csprojbuilds with 0 Warning(s), 0 Error(s). (R1, unverified by R2)Lite.Tests/Lite.Tests.csprojbuilds with 0 Warning(s), 0 Error(s). (R1, unverified by R2)PerformanceMonitor.Darling.Tests.QueryStoreBackfillTests: 12/12 pass, including 3 new tests(the pure
MergeHoleDatabasesunit test, and the Darling + Lite SQL-shape pins). (R1, unverified by R2)Lite.Tests.QueryStoreServerGateTests: 13/13 pass. (R1, unverified by R2)Live test on a seeded hypertable with compressed chunks: new file
Darling/Darling.Tests/QueryStoreBackfillCandidateLiveTests.cs.CandidateDatabases_ServicesAQuietHole_DropsAFinishedDatabase_FindsANewDatabase— a hole on aquiet database (
quiet_hole_db) is still serviced, a finished database (finished_db, no holekey, nothing newer than the floor) drops out, a new database (
new_db) is found, asserted as oneexact-list equality so the candidate method's own exception-swallowing cannot pass on a dead
read.
StoredFloor_ARowAtOrBeforeTheFloor_ReturnsTheFloor_ABackfilledRow_ReturnsItsTrueMin—GetStoredFloorAsyncreturnsfloorLimititself forquiet_hole_db(EXISTS hit) and the truebackfilled
last_execution_time(2026-05-01, notfloorLimit, notcollection_time) fornew_db(EXISTS miss, falls through to the bounded MIN). Compression is bounded witholder_thanso only the old (2026-06-10) chunk compresses, proven againsttimescaledb_information.chunks.is_compressedbefore either test asserts anything else — thebare
show_chunks()call the repair test's own pattern uses would have compressed the hot chunktoo and made the plan check below impossible to pass. (R3a)
Plan check and EXISTS numbers:
CandidateRead_BoundAtItsOwnHorizon_ScansNoCompressedChunkEXPLAINsQueryStoreBackfill.CandidateSqlwith LITERAL values substituted for$1/$2in place ofparameters (a parameterized EXPLAIN can defer chunk exclusion to executor startup and leave
never-executed Decompress nodes in the text) — no
DecompressChunk, noColumnarScan, nomention of the compressed chunk's name, and at least one (the hot) chunk still scanned.
StoredFloorExistsMiss_ForANewDatabase_WalksTheCompressedChunk_MeasuredCostrunsEXPLAIN (ANALYZE, BUFFERS)on the EXISTS text fornew_db(no rows at or beforefloorLimit, so it hasto walk the compressed old chunk before concluding no match — a
Custom Scan (ColumnarScan)onthe compressed chunk is expected and pinned here, not a defect). Measured on this test's tiny
3-row fixture: execution
Buffers: shared hit=3,Execution Time: 0.099 ms; planningshared hit=796 read=4,Planning Time: 9.027 ms. Small in absolute terms because the fixture itself istiny — the pinned fact is the SHAPE (the EXISTS miss does reach the compressed chunk), not the
number, per the ruling not to design against it. (R3a)
Lite twin DuckDB fixture test: flipped
GetBackfillCandidateDatabasesAsyncandGetMinCollectedTimeForDatabaseAsyncfromprivatetointernal(visibility only) and addedLite.Tests/QueryStoreBackfillCandidateTests.cswith the same three database cases and two floorfacts as the Darling live test.
Lite.Tests.QueryStoreBackfillCandidateTests: 2/2 pass. Verifiedboth tests fail against dev's pre-Ring-buffer collectors re-derive their watermark with an unbounded MAX that decompresses every retained chunk each cycle; persist it at write time instead #4197 shape (no
floorLimit/stateparameters, no hole merge,an unconditional
MINwith no EXISTS clamp) by temporarily reverting the bound/merge to thatshape in a local build, confirming both failed, then restoring the fix and confirming green
again. (R3b)
git merge origin/dev(Lite side): merged cleanly, no conflicts, pushed. (R3b)Full Lite suite once, after the
devmerge:Lite.Tests.exe— 5403/5403 pass, 0 errors,0 failed, 0 skipped, 343.4s. (R3b)
git merge origin/dev(Darling side): merged cleanly, no conflicts, pushed. (R3a)Full Darling suite once, after the
devmerge, on a dropped-and-recreateddarlingtest:Darling.Tests.exe— 14056 total, 0 errors, 3 failed, 49 skipped (other PG clusters this rigdoes not configure), 1 not run, 757.8s. All 3 failures were pre-existing census/pin gaps the
Ring-buffer collectors re-derive their watermark with an unbounded MAX that decompresses every retained chunk each cycle; persist it at write time instead #4197 SOURCE change (R1) left stale, not caused by this lane's test-only work:
CommandPlaneCommandTimeoutTests(x2) —GetStoredFloorAsyncnow builds twoNpgsqlCommands(the EXISTS pre-check plus the MIN fallback) instead of one, so the command-plane census went
from 9 to 10 and the hardcoded
ExpectedSiteCount = 9was never bumped; andRepoFileAdoptionTests.ExactlyOneFile_DeclaresTheSharedRepoFileReader(x1) —QueryStoreBackfillTests.csdeclared its own privateReadRepoFileinstead of calling theshared
RepoFile.ReadRepoFile, tripping the "exactly one declarer" adoption census. Both aremechanical and fixed here: bumped
ExpectedSiteCountto 10 (plus its doc comment), and replacedQueryStoreBackfillTests.cs's two calls withRepoFile.ReadRepoFileand deleted the duplicatemethod and its now-unused usings. Re-verified with a targeted run of the 5 affected classes
(
CommandPlaneCommandTimeoutTests,RepoFileAdoptionTests,QueryStoreBackfillTests,QueryStoreBackfillCandidateLiveTests,RepoFileResolutionEquivalenceTests) post-fix: 25/25pass, 0 failed. Did not re-run the full 14056-test suite a second time after the fix (context
budget) — flagging this explicitly so the coordinator can re-run it if a second full pass is
wanted before merge. (R3a)
CI on
1e9ff7bcfailed one test,DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks: theExpectedSiteCountbump inCommandPlaneCommandTimeoutTests.csadded a second<summary>block.3bc7f443merges the two into one.DocCommentHygieneTests77/77 andCommandPlaneCommandTimeoutTests16/16 pass locally. CI decides the rest.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[Bound the Query Store backfill candidate read at its horizon (#4197) #4306]: Bound the Query Store backfill candidate read at its horizon (#4197) #4306
🤖 Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ