Repository navigation
Converge each rollup to its SOURCE, not to raw (#1798) - #1799
Merged
Merged
Conversation
A fully-DONE --backfill-rollups left two hourly-tier retention policies HELD on a lab box, against a contract line that promises zero. The gate was right and the verb's completion claim was wrong. Two notions of "covered" had drifted apart. The #1680 arming gate for an HOURLY-tier policy is SOURCE-relative -- the daily must cover what the HOURLY holds -- while the verb converged every rollup to RAW's oldest row. All four dailies are hierarchical (time_bucket('1 day', bucket) FROM collect.*_hourly), so on a store whose raw purges are ARMED, raw is days deep while the hourlies hold weeks: a daily converged "to raw" stops short, the gate correctly refuses, and DONE is printed over it. The original #1759 profile hid this, because on a held-purge store the hourlies are a subset of raw's span and converging both tiers to raw satisfies the gate by construction. The sharp edge is worse than the symptom: a hierarchical daily added AFTER its hourly on a healthy store enters a hold NOTHING can clear. The pre-raw region exists only in the hourly, and a verb aiming at raw never targets it. RollupViews now carries each rollup's SOURCE and source time column alongside its raw table, and RollupBackfill.Targets is DERIVED from that list rather than restating it -- a second hand-kept copy is exactly the drift surface this issue is about. RawTableFor stays raw for every rollup, dailies included: that answers a READ question (where does a window fall back to) and raw is still the right answer. Only the backfill's convergence target moved. NOTE the issue names three hierarchical dailies; there are FOUR. query_stats_daily reads collect.query_stats_hourly too, verified in its DDL, and would have been left behind by a fix that took the list literally. Found while fixing it, and it is the harder half: the daily's plan was computed from a PRE-BACKFILL snapshot of its source. All plans are taken up front -- necessary, since the disk preflight has to total the whole job before committing to any of it -- but a daily's source is backfilled EARLIER IN THE SAME LOOP, so planning from that snapshot aims the daily at where its hourly USED to start and then judges it against where the hourly NOW starts: SHORT on every daily, every time. The run loop re-probes and re-plans each rollup immediately before running it. Not caught by reading: caught by the verb's own end-to-end test going red, and confirmed as mine by re-running it on the parent commit, where it passes. The DONE contract text is now true rather than reworded: it promises coverage of each rollup's own source, which is what each gate measures. Behaviour pin: a hierarchical daily whose hourly out-reaches raw (raw purged to 4 days, hourly holding 12) must converge to the HOURLY's oldest bucket and the hourly-tier policy must then ARM. No-policy fixture, since the ensure sweep's own refresh policy materializing contiguously already made a pin in this file vacuous twice. Mutation -- point the daily's source back at raw -- verified RED on a COMPILING build, caught both by the source pin and by the fixture-validity guard, each naming the cause. Full solution rebuild 0 warnings / 0 errors. All 37 rollup-backfill tests pass. One unrelated live test (DarlingAnomalyBaselineTests EndToEnd_RestartExclusion...) fails on this local rig and fails IDENTICALLY on the parent with this change stashed -- pre-existing rig state, not this change. CI is the authority. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answer to the review question: (b) — it did NOT bound it, now it does. The preflight totalled up-front estimates (DarlingCliCommands.cs, work.Sum of Plan.EstimatedBytes) while the run re-planned each rollup from live floors. For a hierarchical daily whose hourly is deepened EARLIER IN THE SAME RUN, the re-planned slice set is larger than what the preflight counted, so the run could write more than the number the operator was shown — on exactly the store shape (deep raw, purges held) where free space is tightest. Same invariant class as the rows-per-bucket under-estimate: a refuse/proceed gate has to bound the WHOLE job, and the printed numbers are the commitment. EventualSourceFloor(source_now, root_raw_oldest) takes the DEEPER of the two, because the run converges to whatever the source ends up holding: its floor as it stands, or raw's oldest row, which is as far as backfilling that source can take it. The preflight plans against that; the run keeps re-planning against the live floor. A no-op for the hourlies, whose source IS raw, and it only ever moves the bound LARGER, which is the safe direction for a disk gate. On the estimator's blind spot raised in review: it does NOT count source buckets, so a span the source has not materialized yet cannot read zero. bucketsToAdd is ceil(span / bucketWidth) — pure arithmetic over the planned range — and the bytes-per-bucket calibration comes from the ROLLUP's own materialized sample, never the source's. Extending the range therefore raises the estimate whatever the source currently holds. CLAMP, second half of the same question: it was enforced on the re-planned set (the run calls Plan again, which applies MaxPlannedSlices) but the outcome was MISREPORTED, and that bug was mine from the re-plan commit. RollupBackfillPlan .Absurd sets IsComplete as well as Refusal, and the re-plan branch tested IsComplete first — so a clamp firing mid-run, after the preflight had cleared it under the ceiling, printed "already covers" and counted as success. Refusal is now tested first and reported as a shortfall with a non-zero exit. Pin: printed bound >= actual work, asserted on buckets AND bytes since the operator is shown both, plus a non-vacuity assertion that the pre-backfill floor really does under-count (or the fixture proves nothing) and the hourly/null degradations. Mutation — preflight back to the naive pre-backfill floor — verified RED on a COMPILING build. Full solution rebuild 0 warnings / 0 errors. All 38 rollup-backfill tests pass. Two unrelated live tests fail on this local rig; the PARENT fails two as well, overlapping on one and differing on the other, so the failing set ROTATES between runs on both trees — a degraded local rig, not a deterministic regression. Deliberately not claiming "identical failure" here, because it was not. CI is the authority. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… explicitly Both from the coordinator's independent read of the diff, which reached the same verdict (b) I had. THE PIN, stated their way and it is the better statement. My assertion compared two plans and required the preflight's to be the larger; theirs compares the daily's up-front plan against the HOURLY's up-front plan and requires the daily to start no later. That says the property directly -- the daily must be planned to its source's TARGET, not to where its source currently stops -- instead of inferring it from two numbers. Added alongside the existing ones. Also recorded why the bytes assertion is safe despite their warning against live byte comparisons: it asserts the ESTIMATE, which is deterministic arithmetic over the planned span times a per-bucket sample, not bytes measured off the store. A measured comparison would be flaky exactly as they say -- compression and page slack make the same span occupy different amounts. THE NIT: a refused RE-plan is guarded, not merely improbable. Their transitive argument is right on today's call graph -- a corrupt timestamp refuses the hourly's up-front plan, and since a daily is now planned against its source's eventual floor it refuses up front too, so neither reaches the loop. But that is a property of the current call graph rather than of the code, and what it protects against is slicing a window the planner already rejected as garbage. The guard is three lines and the argument would need re-deriving by every future reader; the comment now records both. Local rig: the rotating shared-store failures were accumulated state, so I rebuilt the test database rather than keep reasoning around them, as promised. That took the suite from two failures to one, and the remaining one (PayloadDimensionLiveTests DimensionGc_Defers...) PASSES IN ISOLATION on the rebuilt rig -- matching what verb-reviewer independently observed on its own box. Shared-store contention, not this change. Full solution rebuild 0 warnings / 0 errors. All 64 rollup backfill + coverage routing tests pass. Suite 3739 passed / 1 failed as described. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erikdarlingdata
changed the base branch from
feature/1759-restore-explicit-refresh-options
to
dev
July 28, 2026 10:16
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 29, 2026
Merged
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.
Closes #1798. Stacked on #1797 — based on its branch, so the diff shows only this change; GitHub retargets to
devautomatically when #1797 merges and its branch deletes. Not based ondevbecause both touchRollupBackfill.The defect
A fully-
DONE--backfill-rollupsleft two hourly-tier retention policies HELD, against a contract line promising zero. The gate was right; the verb's completion claim was wrong.Two notions of "covered" had drifted apart. The #1680 arming gate for an hourly-tier policy is source-relative — the daily must cover what the hourly holds — while the verb converged every rollup to raw's oldest row. All the dailies are hierarchical (
time_bucket('1 day', bucket) FROM collect.*_hourly), so on a store whose raw purges are armed, raw is days deep while the hourlies hold weeks. A daily converged "to raw" stops short, the gate correctly refuses, andDONEprints over it.The original #1759 profile hid this: on a held-purge store the hourlies are a subset of raw's span, so converging both tiers to raw satisfies the gate by construction.
The sharp edge is worse than the symptom. A hierarchical daily added after its hourly on a healthy store enters a hold nothing in the product can clear — the pre-raw region exists only in the hourly, and a verb aiming at raw never targets it.
The fix
RollupViewsnow carries each rollup's source and source time column alongside its raw table, andRollupBackfill.Targetsis derived from that list rather than restating it — a second hand-kept copy is exactly the drift surface this issue is about.RawTableForstays raw for every rollup, dailies included. It answers a read question — where a window falls back to when this rollup cannot serve it — and raw remains the right answer. Only the backfill's convergence target moved.The issue names three hierarchical dailies; there are four.
query_stats_dailyreadscollect.query_stats_hourlytoo (verified in its DDL), and would have been left behind by a fix that took the list literally.The harder half, found by the tests rather than by reading
The daily's plan was computed from a pre-backfill snapshot of its source. All plans are taken up front — necessary, since the disk preflight must total the whole job before committing to any of it — but a daily's source is backfilled earlier in the same loop. So the daily was aimed at where its hourly used to start and then judged against where the hourly now starts: SHORT on every daily, every time.
The run loop now re-probes and re-plans each rollup immediately before running it — the same measured-floor idiom that makes resume work, one level up. The up-front pass sizes the job; this one aims it.
I did not find this by reading. The verb's own end-to-end test went red, and I confirmed it was mine rather than pre-existing by re-running it on the parent commit, where it passes.
Contract
DONEnow promises coverage of each rollup's own source, which is what each gate measures — made true rather than reworded.Testing
Full solution rebuild: 0 warnings, 0 errors. All 37 rollup-backfill tests pass.
Behaviour pin: a hierarchical daily whose hourly out-reaches raw (raw purged to 4 days, hourly holding 12) must converge to the hourly's oldest bucket, and the hourly-tier policy must then arm. No-policy fixture — the ensure sweep's own refresh policy materializing contiguously already made a pin in this file vacuous twice.
Mutation (point the daily's source back at raw) verified RED on a compiling build, caught at two independent points — the source pin and the fixture-validity guard — each naming the cause.
One unrelated live test (
DarlingAnomalyBaselineTests.EndToEnd_RestartExclusion…) fails on my local rig and fails identically on the parent with this change stashed, so it is pre-existing rig state. CI is the authority.Field acceptance
Not run by me: the lab box is deliberately left in its 2-held state as the live reproduction, and the post-merge rehearsal there is the coordinator's.
🤖 Generated with Claude Code