Repository navigation
The startup watermark read reads only the last two days of collection_log, bound as a naive timestamp (#4469) - #4480
Merged
Conversation
…nded MAX/GROUP BY (#4469) The connect-path read of every collector's last run touched every retained collection_log chunk, compressed ones included, before it could find the newest instant per collector. On the busiest measured store that took 4,775 ms and read 21,483 buffers against a 32-chunk hypertable; a literal 2-day floor on collection_time drops that to 69 ms with the same statement shape, because TimescaleDB can now exclude every older chunk before opening it. The floor is safe because this read only seeds each collector's next-due time from its own cadence: a collector whose true last run predates the floor is, by definition, already overdue on every schedule this product ships (the longest recurring cadence is 1 day), so it falls back to the same never-run seed the read already used on an outright failure.
#4469) - The floor parameter now binds via DateTime.SpecifyKind(..., Unspecified) instead of the default Kind=Utc mapping, matching the naive timestamp column collection_time and the same product-wide contract other binds use (BindActualPlanResolveParameters). Confirmed on a live TimescaleDB rig: the bounded plan still shows chunk exclusion on the recent chunks, with the comparison as timestamp >= timestamp (no cast on collection_time). - Added DarlingWatermarkFloorScanBoundLiveTests, a runtime pin driven only through the existing product method ReadCollectorWatermarksAsync (no new member reference), which snapshots pg_stat_user_tables scan counters (including each compressed chunk's compression_settings.compress_relid relation) before and after the call and asserts no chunk older than the floor shows a new scan, plus that the returned watermarks equal the unbounded oracle. This test fails at RUNTIME against the pre-fix method, which cannot avoid scanning every chunk.
… floor's measured effect honestly (#4469)
erikdarlingdata
marked this pull request as ready for review
September 27, 2026 17:24
This was referenced Sep 27, 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 #4469. The floor removes the older-chunk part of the cost; the newest-chunk part is a follow-up.
Why
The connect-path watermark read (
DarlingWorker.ReadCollectorWatermarksAsync) used to run anunbounded
MAX(collection_time) GROUP BY collector_name, which cannot answer without readingevery retained chunk, compressed ones included, because the newest instant per collector is not
known until the whole table has been read. The PostgreSQL log on the busiest measured store
showed the statement cancelled at its 10 s client deadline 12 times over about 3 days, mostly
just after a service start — the other cancels logged there were confounded by other load on
that host and are not counted here.
A field EXPLAIN on that store, run back to back (unbounded, then the bounded statement right
after), gives an honest before/after only for the read pattern, not the timing: the unbounded
run was cold — 4,775 ms, 3,166 hit + 21,483 read buffers, 13,957 ms of parallel-worker I/O read
time — while the bounded run right after it was warm (69 ms, 20,703 buffers, all
shared hit),because it read the same pages the unbounded run had just pulled into cache. Comparing those two
numbers directly overstates what the floor buys.
The real split, from the unbounded run's own EXPLAIN: 89% of its I/O read time (~12,417 ms) was
a Bitmap Heap Scan over the two newest, uncompressed chunks (~28,000 and ~19,000 rows for one
server, spread across ~12,000 and ~8,000 heap blocks) — cost the floor does not touch, because it
keeps the newest chunks. Only 11% (~1,540 ms) was spent walking the 30 older compressed chunks,
which the floor excludes outright via chunk exclusion on
collection_time. That 11% grows withretention and chunk count, so the floor is still worth having, but it is not the dominant cost on
this read, and a follow-up is needed for the newest-chunk heap-fetch cost.
The separate
collector_stateprune on this connect path is out of scope: measured on the samestore, its statement costs well under a millisecond.
The floor and its parameter type
ReadCollectorWatermarksSqladdsAND collection_time >= $2to the sameMAX ... GROUP BY, with$2 = now - WatermarkFloorLookback(2 days; the longest recurring collector cadence is 1 day, and acollector whose last run is older than the floor is seeded exactly as an overdue one is). The floor parameter binds
DateTime.UtcNow - WatermarkFloorLookback. Npgsql's default mappingfor a
Kind=UtcDateTimeistimestamptz, butcollection_log.collection_timeis a naivetimestamp— the product-wide contract (seeBindActualPlanResolveParameters's naive-UTC bindfor a query snapshot's collection_time, and
RawChunkIntervalReconciler's comments on the samecontract). Binding a
timestamptzagainst a naive column forces a cast on one side, which candefeat the chunk exclusion the floor exists to get.
The bind is
DateTime.SpecifyKind(DateTime.UtcNow - WatermarkFloorLookback, DateTimeKind.Unspecified), sent as a plaintimestamp, matching the column.Confirmed on a live TimescaleDB rig with a 32-chunk hypertable (30 compressed) that the plan gets
chunk exclusion, with no cast on
collection_time:The filter and index condition compare
collection_time/_ts_meta_v2_first_collection_time(both naive
timestampcolumns) against a bare, uncasttimestampliteral — no::timestamptzcast anywhere in the plan — and only the newest chunks execute (DarlingWatermarkFloorPlanShapeLiveTestsasserts the touched-chunk bound on the plan).The runtime pin
DarlingWatermarkFloorScanBoundLiveTestsis a live test driven ONLY through theexisting product method
DarlingWorker.ReadCollectorWatermarksAsync(postgres, serverId, logger, ct)— no reference to the newReadCollectorWatermarksSqlorWatermarkFloorLookbackmembers,so it compiles unmodified against a build that predates both.
It seeds 30 days of
collection_loghistory (most chunks old enough to compress, the fieldshape), snapshots every chunk relation's
pg_stat_user_tablesscan counters — for a compressedchunk this means the relation named by
_timescaledb_catalog.compression_settings.compress_relid,since that is where TimescaleDB's actual scan against compressed data lands — calls the method,
and asserts (a) the returned watermarks equal the unbounded oracle, same as the existing
plan-shape test, and (b) not one chunk older than the floor (minus a chunk width of slack) shows
a new scan afterward.
RED against
origin/dev(this test file copied unmodified into a detached worktree,building and running there unchanged): the pin fails at RUNTIME, not at compile time. The old
method has no floor, so an old chunk's (seq_scan, idx_scan) counters move (one new index scan):
GREEN on this branch:
The mutation (removed the
AND collection_time >= $2predicate fromReadCollectorWatermarksSql, rebuilt, reran):Reverted immediately after; the diff in this PR does not carry the mutation.
Test plan
DarlingWatermarkFloorPlanShapeLiveTests,DarlingWatermarkSeedLiveTests,DarlingWatermarkFloorScanBoundLiveTests(new),DocCommentHygieneTests: 80/80 passingtogether on a live TimescaleDB 2.30.1 rig, 0 warnings on the build.
origin/dev(above).DocCommentHygieneTests77/77;Darling.Testsand
Lite.Testsbuild with 0 warnings.CHANGELOG
SECTION: Changed
ENTRY:
collection_logchunk in retention ([The startup watermark read reads only the last two days of collection_log, bound as a naive timestamp (#4469) #4480]) - it reads only the last two days, which is all the schedule seed needs.REF:
[The startup watermark read reads only the last two days of collection_log, bound as a naive timestamp (#4469) #4480]: The startup watermark read reads only the last two days of collection_log, bound as a naive timestamp (#4469) #4480