Repository navigation
Event-windowed reads: bound by collection_time so TimescaleDB skips out-of-window chunks (#4229) - #4235
Merged
Conversation
#3895 (#4229) BlockingTrendSql, SystemHealthEventsByTypeSql, GetJobHistoryAsync, DefaultTraceEventsByWindowSql, BlockingDurationStatsSql, BlockingPairRowsSql and MemoryPressureEventsSql (viewer + MCP/service twins where they exist) window only on an event's own timestamp, so on a hypertable partitioned on collection_time they scan every retained chunk. Each now also carries AND collection_time >= EventWindowFloor.For(windowStart), one chunk before the window, bound as a parameter so the planner can exclude chunks. Results are unchanged: an event is always collected at or after it happens, within the floor's one-chunk skew allowance. GetJobHistoryAsync's SQL builder is split into a testable BuildJobHistorySql(bool) so both the fleet-wide and single-server parameter shapes can be pinned without a live Postgres. Source-only checkpoint; pin tests, live tests and rig EXPLAIN numbers follow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
EventWindowedReadsCarryTheFloorTests enumerates every statement/twin touched by the previous commit (BlockingTrendSql, SystemHealthEventsByTypeSql, DefaultTraceEventsByWindowSql/EventsByWindowSql, BlockingDurationStatsSql, BlockingPairRowsSql, MemoryPressureEventsSql, both BuildJobHistorySql parameter shapes) and asserts each event-time scan carries its own "collection_time >= $N" predicate, counting scans rather than just detecting one, since the two-CTE XE/DMV reads have two. Positive controls hold each statement's pre-#4229 text and must count zero, so the rule is proven non-vacuous on the tree this issue was filed against. 16/16 pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…y and chunk exclusion EventWindowedReadsAreBoundedLivePostgresTests seeds each of the six #4229 read groups' tables past 3 days of chunks and proves both halves the string-level pin cannot: the floored statement returns exactly what its unfloored predecessor returned (including a late-collected row and a row whose event clock ran ahead of its collection), and the floor lets TimescaleDB exclude chunks it plans against the oracle's own footprint rather than a hard ceiling, since darlingtest is a store every live class shares. Updates ViewerMemorySqlTests' pre-#4229 assumption that MemoryPressureEventsSql never mentions collection_time. 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:11
This was referenced Sep 25, 2026
Job History: GROUP BY join replaces the per-job window function, tab off the 30s timer (#4229)
#4256
Merged
erikdarlingdata
added a commit
that referenced
this pull request
Sep 25, 2026
…of (flake from #4235) (#4275) EventWindowedReadsAreBoundedLivePostgresTests failed dev's Build runs 36115696476 and 36117118128 on job_history's AssertChunksAsync. #4229's GROUP BY fix split the floored job_history read into two independently floor-bounded scans (job_stats CTE plus the base join), so the floored plan now visits every in-window chunk twice. PlanChunkScans.Count counts scan nodes, not physical chunks, so that doubling ate the two-chunk reduction margin the test asserted, even on a freshly created database with nothing else in it (measured old=7, new=6 raw nodes, reduction 1). Add PlanChunkScans.DistinctChunkCount and use it in AssertChunksAsync so a chunk scanned twice by two Append branches over the same hypertable counts once. Measured after the fix: old=7, new=3 distinct chunks, reduction 4, comfortably clearing minReduction: 2. Confirmed the assertion still catches a real regression: with the floor neutralized for one read, old=7, new=7, and the test correctly fails. Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
3 tasks
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 #4229.
Lane V2b. Lane V2 finished the SQL source changes and the string-level category pin for all 6 read groups. It did not stand up a rig or run anything live. This PR adds the rig verification, the live-Postgres correctness proof, all required gates, and a full UTC suite run.
Why
#3895 added
EventWindowFloorfor one problem: a read windowed only on an event's timestamp (event_time,deadlock_time) gives TimescaleDB nothing to exclude a chunk on. The table is partitioned oncollection_time, not the event column. The floor adds acollection_timelower bound the planner can act on. An event is always collected at or after it happens, so the floor cannot drop a qualifying row. That fix covered the fleet overview reads only.Six more reads listed in #4229 have the same shape and never got the floor:
BlockingTrendSql,SystemHealthEventsByTypeSql,GetJobHistoryAsync,DefaultTraceEventsByWindowSql,BlockingDurationStatsSqlwithBlockingPairRowsSql, andMemoryPressureEventsSql.What changes
Every read above, and its MCP/service twin where one exists, now carries
AND collection_time >= $N. The parameter isEventWindowFloor.For(windowStart): naive UTC, one chunk before the window. It is bound as a parameter, not an expression, so the planner can see it and exclude chunks. The event-time predicate itself is untouched.BlockingTrendSql(viewer andDarlingBlockingTrendReader). Both the XE and DMV-fallback CTEs get their own floor.SystemHealthEventsByTypeSql(viewer andDarlingSystemHealthReader). One shared statement covers all 9 System Events readers and 10 sub-tabs. Both texts are pinned identical.GetJobHistoryAsync, split into a testableBuildJobHistorySql(bool scopedToServer). The floor binds againstcollection_timedirectly, not the de-skewedrun_datetimeexpression.DefaultTraceEventsByWindowSql(viewer) andDarlingDefaultTraceReader.EventsByWindowSql. The floor binds againstcollection_timedirectly rather than the de-skewed expression.BlockingDurationStatsSql(viewer andDarlingDataReader). Same two-CTE shape asBlockingTrendSql.BlockingPairRowsSql(viewer only, consumed only by the block-chain viewer UI).MemoryPressureEventsSql(viewer only). Windows onsample_time, floored the same way.Two items from the issue are out of this PR (see "Not mine" below). The Job History
GROUP BYrewrite and 30-second timer change belong in a later PR.GetAgentStatusAsync'sLATERALrewrite is #3947.Job 1: rig numbers
Seeded
probe(a scratch Postgres database on the rig, migrated viaPgMigrations) with 4 days of out-of-window history and 3 in-window rows per table. RanEXPLAIN (ANALYZE, BUFFERS)for a 24-hour window, old text vs. new.This is a small synthetic seed (1-2 rows per chunk). The absolute buffer/row counts are tiny. What it isolates is the floor's actual mechanism: fewer chunks opened at plan time. The issue's field numbers (204 ms to 5 ms) are the production-scale evidence. This table confirms the mechanism.
BlockingDurationStatsSql's old planning time is an outlier low. It ran right after BlockingTrendSql's identical tables in the same
psqlsession, with a warm catalog cache. Chunk count and buffers are the reliable columns across the whole table. The two-CTE reads plan up to 3 chunks per CTE for a 24-hour window. That is the window's own 1-2 chunks plus the floor's one extra day, doubled because two hypertables are scanned. The issue's "3 chunks or fewer" wording covers a single-table scan.Job 2: the issue's live pin
On that same seeded
probestore,EXPLAINofSystemHealthEventsByTypeSqlfor the 24-hour window touches exactly 3 chunks with the floor and 6 without. Both counts are reproduced automatically byEventWindowedReadsAreBoundedLivePostgresTests(below). It runs both texts every time and asserts the floored one is substantially smaller than the oracle's own footprint.Job 3 and the rest of Job 2:
EventWindowedReadsAreBoundedLivePostgresTestsNew live-Postgres test class,
Darling/Darling.Tests/EventWindowedReadsAreBoundedLivePostgresTests.cs,[Collection("live-postgres")]. Seeds 4 days of out-of-window chunks plus 3 in-window rows per table. The 3 in-window rows cover a normal case, a row 20 hours late to collection, and a row with the event clock 50 minutes ahead. All sit well insideEventWindowFloor.SkewAllowance.TheFlooredReads_ReturnExactlyWhatTheUnflooredReadsReturned: for each of the 7 statement shapes, the floored text and its pre-Reads windowed on an event timestamp have no collection_time floor, so they scan every retained chunk: system health 204→5 ms (10 sub-tabs), default trace 169→25 ms, Job History 1.9→1.2 s (WPF viewer and web/MCP twins) #4229 text return byte-identical row sets. Count is asserted non-vacuous.TheFlooredReads_PlanAtMostThreeChunksForA24HourWindow:EXPLAIN (COSTS OFF)for each group, asserting the floor excludes at least 3 of the 4 seeded old-day chunks. This is a relative assertion (floored chunks far below the oracle's own chunk count), not the issue's literal "3 or fewer". Thedarlingteststore is shared by every live class in the suite. Other classes seed rows with near-"now" timestamps that can land inside this test's floor window and inflate its chunk count.FleetReadsAreBoundedByTheFleetTestshit the same problem and used the same fix. I confirmed by hand against a fresh database that the floored system-health plan touches exactly 3 chunks for a 24-hour window. That is the literal proof of the issue's pin. The committed regression test uses the relative form so it stays green on the shared store.Two existing tests were casualties of this change, both mine to fix.
ViewerMemorySqlTests.MemoryPressureEventsSql_WindowsOnSampleTime_SelectsIndicatorsasserted thatMemoryPressureEventsSqlnever mentionscollection_time. The floor makes that false. Updated to assert the floor predicate instead of its absence.LivePostgresCollectionHygieneTestsfailed on the new class for missing a shared-store declaration. Added[Collection("live-postgres")].Job 4: Lite parity
Lite's DuckDB store has no hypertable or chunk concept and no retention partitioning like TimescaleDB. It is a single local file per machine. DuckDB's row-group min/max zonemap pruning happens automatically per-column at scan time. It does not require a predicate on a declared partition column the way TimescaleDB's chunk exclusion does. The failure mode #4229 fixes has no DuckDB-side analog.
A
collection_timebound helps DuckDB skip row groups time-clustered by insertion order. This lane did not measure that. The bound is not ported to Lite's copies of these reads without evidence it does anything. A follow-up lane can measure it directly inLite.Tests(DuckDB runs in-process there, no rig needed).Job 5: gates and the full suite
DocCommentHygiene,StorageCommandTimeoutTests,FleetReadsAreBoundedByTheFleetTests(and its live class),McpPayloadContractCensusTests,EventWindowedReadsCarryTheFloorTests,EventWindowedReadsAreBoundedLivePostgresTests: 195/195 pass together.git merge origin/dev: clean, no conflicts.Darling.Testssuite, rig set to UTC, freshdarlingtest: 13,809 total, 0 failed, 47 skipped, 1 not run.ServerListAndSummaryPlanShapeTests,CaptureDownChunkOrderTests, andViewerW2aLivePostgresTests.ServerSummary_...all passed. They were a local time-zone artifact, not regressions from this PR.Lite.Tests: not run. No Lite source changed this PR.Not mine
Two items from the issue stay out of this PR, per the brief. The Job History
GROUP BYrewrite and 30-second timer change belong in a later PR after this one merges.GetAgentStatusAsync'sLATERAL ... LIMIT 1rewrite is #3947.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[Event-windowed reads: bound by collection_time so TimescaleDB skips out-of-window chunks (#4229) #4235]: Event-windowed reads: bound by collection_time so TimescaleDB skips out-of-window chunks (#4229) #4235
Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ