Repository navigation
The CPU collector's watermark and dedup key are the UTC twin where the store has it, with the frame stated, so the autumn fall-back's repeated local hour lands instead of being dropped as already collected (#3778) - #3789
Merged
Conversation
…e store has it, with the frame stated, so the autumn fall-back's repeated local hour lands instead of being dropped as already collected (#3778) The ring-buffer arm dedups client-side against MAX(sample_time), a LOCAL wall-clock instant, and drops every sample at or below it — which from the fall-back instant is every sample for an hour, on every non-UTC server, once a year. The other half of #3744's blind hour; invisible because #3730's live straddle test planted through WritePayload, below the dedup. ICollectorDefinition gains UtcWatermarkColumn (base null; only cpu_utilization declares it, as sample_time_utc) and CollectorContext gains WatermarkFromUtcColumn. Both hosts branch on the declaration: a definition without a twin goes through the unchanged GetLastCollectedTimeAsync and its byte-identical SQL; a definition with one goes through GetLastCollectedTimeWithFrameAsync, ONE statement reading MAX(twin), MAX(local), handing back the twin in the UTC frame where any row carries it and the local maximum in the LOCAL frame otherwise. The collector compares each row in the watermark's frame. The first post-upgrade run finds no twin and dedups local-to-local exactly as before (zero duplicated, zero dropped beyond the old rule); every run after it is UTC-to-UTC. Not the gate, not the de-skew readers, not sample_time's frame, not Azure (server-side filter on end_time, UTC in both columns; query text and parameter pinned identical in either frame). No rung. Pins: the three acceptance sequences through the real ReadAsync (fall-back hour lands under UTC and the local control lands zero; the upgrade day at zero duplicates; Azure identical), the never-path kept-not-guessed, the census (only cpu_utilization declares the twin, none beside a PerDatabaseWatermarkColumn); both hosts' SQL as shipped strings and their branches as source; the DuckDB pair read on a real store; the PostgreSQL round trip reading the runner's pair read at three moments; the #2797 IL gate pin tracking both readers.
erikdarlingdata
enabled auto-merge (squash)
September 20, 2026 14:10
erikdarlingdata
disabled auto-merge
September 20, 2026 14:14
This was referenced Sep 20, 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.
The lie
The CPU collector's watermark was the monitored server's LOCAL
sample_time, and its ring-buffer dedup dropped every sample at or below it. From the autumn fall-back instant the local clock repeats an hour, so every sample for the next hour carried a stamp at or below the newest one stored before the transition and was discarded as already collected — nocpu_utilization_statsrow for roughly an hour on every non-UTC server, once a year. #3744 / #3774 fixed the alert GATE's half (identity by equality); this is the collector's half, and it was invisible because #3730's live straddle test planted its batch throughWritePayload, below the dedup.Mechanism
CpuUtilizationCollector.ReadAsynccannot filter its computed stamp in SQL, so it dedups client-side againstcontext.Watermark, which both hosts read asMAX(<WatermarkColumn>)—MAX(sample_time), a local wall-clock instant. Verified at source on dev050b50e1:CpuUtilizationCollector.csWatermarkColumn => "sample_time"andsampleTime <= context.Watermark.Value;DarlingCollectorRunner.GetLastCollectedTimeAsync/BuildServerWatermarkSql; LiteRemoteCollectorService.GetLastCollectedTimeAsync. Column 3 of every arm issample_time_utc(V134 / Lite v63), the same instant in UTC, NULL only on rows written before the rung.The fix — the watermark's frame is carried, not guessed
A new definition member,
ICollectorDefinition.UtcWatermarkColumn(base defaultnull; onlycpu_utilizationdeclares it, assample_time_utc), and a new context member,CollectorContext.WatermarkFromUtcColumn(init-onlybool, false unless the host sets it).GetLastCollectedTimeAsyncand its byte-identical SQL. A definition WITH one goes through a newGetLastCollectedTimeWithFrameAsync(Darling, with the samecollectedSincebound and explicit command deadline as its sibling; Lite, DuckDB), which runs ONE statement —SELECT MAX(sample_time_utc), MAX(sample_time) FROM cpu_utilization_stats WHERE server_id = $1— and returns the twin's value withFromUtcColumn = truewhen any row carries it, else the local maximum withfalse, else(null, false): the first-run fallback exactly as before.SampleTimeUtcagainst a UTC watermark,SampleTimeagainst a local one. UTC never repeats an hour, so under a UTC watermark the repeated local hour lands sample by sample.Why this shape and not the alternatives
The issue named three shapes.
MAX(sample_time_utc)is NULL on a store with no post-rung rows, and a NULL watermark is the first-run fallback — re-pointing the watermark outright would re-collect the wholeTOP (60)under stamps the store already holds, a duplicate hour per server, once. ACOALESCEinside oneMAX, or converting the local watermark by the batch's ownsample_time - sample_time_utcoffset, each compare across frames on the upgrade run; the best bound either can offer is one offset's worth of samples. The stated frame's bound is zero: the first post-upgrade run finds no twin, gets the local watermark and compares local-to-local — the pre-#3778 rule exactly, nothing duplicated, nothing dropped beyond what that rule always dropped — stores rows carrying the twin, and every run after it is UTC-to-UTC forever. The twin wins whenever it exists without comparing the two maxima, because comparing them would itself be the cross-frame comparison this exists to avoid (and in the fall-back shape the local maximum belongs to an OLDER sample — the live test plants exactly that).The mechanism is generic (a second watermark column with its frame) rather than a CPU-only special case, and its boundary is stated: honoured on the server-scoped read only; the per-database and per-item refreshes read the declared column alone, so a definition must not declare a UTC twin together with a
PerDatabaseWatermarkColumn— pinned.What nobody promised: a fall-back that straddles the FIRST post-upgrade run itself is dropped exactly as it would have been the day before, because that run is still on the local rule. One run, one server class, one hour, and the run after it is on UTC. The class remarks carry the upgrade-day story beside the #3730 essay rather than contradicting it.
What it does NOT do
sample_time's frame: it stays the server's local clock, the display frame Lite's chart plots, and both clock pins inCollectorTimestampFrameTestshold unchanged.@last_sample_timeagainstdrs.end_time, which is UTC and is the value of BOTH stored columns there; the query text and the bound parameter are pinned identical in either frame, and the Azure arm never dedups client-side in either.cpu_utilizationis the only declaring definition (census-pinned) and the non-declaring branch calls the untouched sibling with its exact-string-pinned SQL.Tests
Executed on this Mac in
Lite.Tests- andDarling.Tests-named net10.0 harnesses (Collectors is net10.0; the DuckDB reads run against a real DuckDB; the PostgreSQL round trip ran against a throwaway TimescaleDB 2.28.1 / PG 18 store migrated to V135). Each behavioural pin was shown red under a deliberate mutation (the old local-only dedup; the pair read preferring the local column; the DuckDB SQL with its columns swapped) and green on the fix.Lite.Tests/CpuUtilizationCollectorDefinitionTests— the three acceptance sequences through the REALReadAsync: (1) America/New_York 2026-11-01, 62-row ring buffer newest-first, all 60 repeated-hour samples land under the UTC watermark and the two already-stored ones are dropped; the CONTROL under the local watermark lands zero (the mechanism is the frame, not the value). (2) The upgrade day: eleven pre-rung rows, run 1 on the local rule lands 12:11–12:12 with zero re-landed, run 2 finds the twins and is on UTC, fifteen distinct samples each exactly once. (3) Azure: identical query text and parameter in either frame, no client dedup in either. Plus the local-frame and UTC-frame dedup pins side by side, the kept-not-guessed never-path (NULL twin under a UTC watermark), and the census: onlycpu_utilizationdeclaresUtcWatermarkColumn, none declares it beside aPerDatabaseWatermarkColumn.Lite.Tests/TimeHonestyRungTests— the runner-branch source pin and both DuckDB SQL strings;WatermarkFrameReadTestson a real DuckDB through the real protected reads: empty store, pre-rung only (local, LOCAL), one post-rung row flipping the frame even when a pre-rung local stamp is LATER (the in-hour upgrade shape), scoped per server, and the plain read unchanged beside it.Darling.Tests/TimeHonestyRungTests—BuildServerWatermarkSqlpinned as the exact strings it was (bounded and unbounded) andBuildServerWatermarkPairSqlas the exact new strings; the runner-branch source pin; both readers' signatures (the pair read acceptscollectedSince, soServerWatermarkReadFloorTestscovers it by construction). The live round trip now reads the runner's pair read at three moments: pre-rung rows only (local, LOCAL), after the real COPY has stored the twin (the newest INSTANT in UTC while the plain read still answers the EDT-side row's later local stamp — the fall-back shape), and after the post-rung rows are gone again.Darling.Tests/CollectorTimestampFrameTests— prose updated; a frame-of-dedup source pin beside the frame-per-column clock pins.Darling.Tests/DarlingCollectorRunnerTests— the The server-scoped watermark read is discarded entirely for collectors that declare a per-database watermark #2797 IL gate pin now tracks both server-scoped readers, and asserts the pair read is actually called somewhere.Closes #3778