Re-derive the refresh grid so contention cannot depend on list position, and re-derive the compression grid with it - #3178
Conversation
…ory census The constant was 594 s, the maximum of a 16-run record that mixed a census of the boundary day's tail with a sample of the days after it. The census read the comment named as the fix has been done: every run of job policy_refresh_continuous_aggregate query_store_stats_interval_hourly since the 13:44:23 boundary, one row per run from timescaledb_information.job_history, read at 2026-09-08 01:37Z. 57 succeeded runs after the boundary, 194 s to 896 s, median 418 s, none failed and none without a finish time; 304 runs at or before it, median 1081.7 s, maximum 13300.7 s. The estimator is unchanged, so the constant is 896. Two pre-registered triggers fired together. The 95th percentile is no longer the maximum at 57 readings (830 s, 66 s below it), so the sample-size half of the maximum-versus-percentile argument is retired and the decision is re-taken on the half that never referenced n. And the sizing figure now sits 146 s ABOVE RefreshSlotWarningSeconds, so every assertion holding the grid clear of it is false. Those are left with their conditions exactly as written.
… state it At 896 s of a 900 s slot the refresh still fits, so "inside one phase slot" is true and load-bearing; the margin that made it "well inside" is 4 s and naming it as comfort is the claim that has stopped being true.
Three of the six failing methods carried a comment explaining why they are left failing and which decision owns the remedy; three did not, so the same inversion read as an oversight in half the places it appears. Conditions are unchanged in all six. NoCompressionMinuteStartsWhileTheHeaviestRefreshIsStillRunning also needed the distinction its two halves now have: the grid clearance genuinely passes (the nearest compression minute is 1,320 s past the heaviest start), and only the watch-line ordering and the 26%-gap literal derived from it are false.
… asserted The open-question paragraph closed on "the excluded run is disqualified by its duration alone", which was the belt to the boundary's braces. At a 896 s population maximum 864 s is an ordinary member of the post-boundary range, so that belt is gone and the sentence was contradicting the paragraph above it. The positional rule needs no second reason, which is why it was chosen over one.
… into fix/3174-refresh-grid-derivation
The hourly phase map is injective now: the heaviest refresh is answered by identity and every other policy takes its own consecutive minute, so thirteen policies hold thirteen distinct minutes at any list order. The hour is three derived bands - the light band and its guard, the heaviest refresh's window, the compression band - and the window is the remainder, so nothing sizes itself from the ceiling it has to hold. The compression grid is re-derived from the same rule rather than renumbered, the guard band comes from the light refreshes' own ceiling, and the band's width comes from the catalog and a stated per-minute spread.
… census 226.8 s over 874 runs of the twelve non-heaviest hourly refresh policies, post-boundary, with nothing removed by the succeeded/finish filter. The midnight regime stays in, because the guard band is now derived from this figure and a readout of a margin has to include the runs that consumed it. The census's shape, the midnight mechanism behind the two largest runs, and the guard band the ceiling sizes are all pinned against the constants rather than restated.
|
Reviewed this PR (Darling-only C# change: Scope / parity: No T-SQL and no Lite changes here — the hourly refresh/compression grid is a TimescaleDB-specific concept that only exists in Darling's PostgreSQL store, so there's no Lite counterpart to keep in parity with. Math check: I worked through the re-derived grid by hand against the constants — One finding posted inline: a stale XML doc comment on Security / injection: no new SQL string interpolation from external input — the grid math is all constants/derived catalog counts, and the SQL-building helpers ( Performance: Everything else in the diff is XML-doc/test-comment prose re-derivation to match the new grid; I didn't find further code-level defects beyond the one inline comment. |
…slot PercentOfSlot's doc quoted 99.6%, which was 896 against a 900 s slot; against the window the hour can spare it is 71.1%, which is what the test asserts. The narrowing pin's margin, the population clause's warning-band note and one watch-line figure were reading the same way.
Review summaryReviewed the diff against Correctness — hand-verified the grid arithmetic independently of the tests:
Security: No new SQL-injection surface — the only inputs to the SQL-building helpers ( Performance: One non-blocking design note: No correctness bugs, parity issues, or security concerns found. The doc-comment/test pinning is unusually (impressively) exhaustive and internally consistent everywhere I checked it by hand. |
timescaledb_information.job_history only records executions where timescaledb.enable_job_execution_logging is on, it is off by default, and the conf block that sets it cannot be healed onto a cluster that predates it - so a maximum over that view on an older store returns zero rows and reads as nothing exceeded the line. Both censuses are one store's, said as a precondition of the read rather than as a caveat about the workload. The refresh-slot warning line names that route, so it now names the condition too: a pin on a pointer's presence cannot tell an operator that following it may return nothing.
ReviewThis PR only touches Darling's C# side ( Arithmetic check. I independently recomputed every derived constant the PR's description and doc comments claim, rather than trusting the prose:
All of this holds together and matches the numbers asserted in One finding posted inline ( No correctness, security, or performance issues found in the actual logic — the constants, the injective phase map, and the compression-exclusion predicate are all internally consistent and match the extensive test pins. |
The rotation control re-implemented the counting rule inline, because the public map can only be called at the one order HourlyRefreshPhaseOrder has. That proved the rule injective under permutation and left the shipped method exercised at exactly one order. The map now takes the order as a parameter and the product passes its own list. Only the order is a parameter: the geometry still comes from the registry, so the seam cannot fabricate a different grid.
Comparing the two minute sequences passes for an overload that ignores its order parameter, because that still returns a permutation of the unrotated minutes. What has to change is the minute a named view gets.
|
Reviewed the diff (DarlingConfig.cs, TimescaleSupport.cs, and the four Darling.Tests files). Summary, since the actual code delta is small relative to the doc-comment volume (~150 lines of real logic/test-assertion changes vs. ~1,400 lines of XML doc prose): Correctness — traced the core algorithm changes by hand and they check out:
Lite/Darling parity — not applicable here. This whole change (TimescaleDB continuous-aggregate refresh/compression phase scheduling) is Darling/Postgres-only; grepped Lite for any equivalent concept and found nothing, so there's no counterpart that could have drifted. Security — no new external input, no SQL string changes, no injection surface. The T-SQL style (CONTRIBUTING.md) — no Performance — No correctness, parity, security, or performance issues found. One non-blocking observation: the doc-comment-to-code ratio is very high (single constants carry 60+ line |
Review summaryReviewed this against correctness, Lite/Darling parity, security, and performance. This PR only touches Correctness — verified by hand:
Test suite: No correctness, security, parity, or performance issues found. The computed properties recompute a handful of small values (bounded by collector/hypertable counts) rather than reading true constants, but they're only invoked from periodic alert-evaluation paths and a one-time static initializer, so there's no meaningful performance regression. |
|
Reviewed the diff (all six changed files are under Parity: No Lite/Darling drift. This is TimescaleDB continuous-aggregate refresh scheduling, a concept that only exists on the Darling/Postgres side — Lite (DuckDB) has no continuous aggregates and, per the existing comment in Core logic ( Provenance-pin tests: Security / performance: No new SQL, no new I/O or external input handling. No correctness, parity, or security issues found. Given the scale of this PR (~1500 lines, mostly doc-comment/test provenance pinning) I did not hand-verify every one of the 57 quoted census figures by hand — that's exactly what |
…sent tense (#3181) * State what the refresh-slot routine-band case establishes, in the present tense The comment above the InsideSlot case narrated a failure that no longer exists: it said the recorded ceiling logs Warning rather than Debug and that the assertion below it fails. Both are false on the current grid, and the assertion passes. The ceiling is 896 s against a 1,260 s slot and a 1,050 s watch line, so ClassifyRefreshSlotHeadroom bands it InsideSlot and LogHeaviestRefreshSlotHeadroom takes the Debug arm. The replacement states that, and keeps the reason the case is fed the grid's own sizing figure rather than a lower literal: both sides are read from TimescaleSupport, so the case goes red if a census raises the ceiling or a re-derivation narrows the slot until the product would call its own sizing figure a warning. The assertion and the value it is fed are unchanged. * Cite the issues the ceiling, slot and watch line come from CONTRIBUTING.md's comment convention asks for issue numbers alongside the measurements. #3178 sets all three figures; #3166 is how a census raises the ceiling and #3174/#3178 how a re-derivation narrows the slot, which are the two changes that would turn this case red.
…of the PR bodies The entry text does not only ride the PR description. #3150's own account of the previous batch says 11 of its 66 entries came from a lane's handoff file in `~/Documents/pm-issue-queue-inbox/inbox/` rather than the body, and three files there carry dedicated entry text inside this range: `3164-changelog-entry.md`, `3166-changelog-entry.md` and `3169-changelog-entry.md`. Searching only the bodies missed the first two. - **#3164** (PR #3176) is no longer written from the PR description. Its lane wrote a full entry, and that text is used verbatim: it carries the 1,036 blocks/s upper bound, the reason the decoupling #3153 deferred is unreachable, the declined command-deadline route, the census deliberately not restated at the new ceiling, and the mutation that caught nothing. `[#2997]` joins the definition block at its ascending position, which that text cites and the file did not define. - **#3166** (PR #3168, absorbed and superseded by #3178) had no entry at all. #3178 grouped it into its own citation with a one-line summary, but its lane wrote a separate entry for the census that took `HeaviestHourlyRefreshObservedCeilingSeconds` from 594 s to 896 s and inverted the watch-line ordering. It sits beside #3178's, on #3178's merge. - **#3191's entry** said "#3184 named four of the six". #3184 was squash-merged and the four-of-six state never reached `dev`; it was a commit inside that PR, corrected before merge. "shipped" in this file means merged, so the clause now says a commit in #3184, which is what the #3187 lane's own report establishes. `CHANGELOG.md` only, 60 insertions and no deletions against `origin/dev`: 33 entries (6 Added, 3 Changed, 24 Fixed) and 27 definitions. All 15 definition runs keep the ordering they had, 9 of 15 ascending before and after.
…ix merged pull requests (#3213) * Record the CHANGELOG entries for thirty-one changes across twenty-five merged pull requests Applies the `[Unreleased]` entries for the pull requests merged to `dev` from #3150 (the previous batch) through #3203. Lanes do not edit `CHANGELOG.md` -- every one of them appends to the same block, so a per-PR edit conflicts with whichever sibling merges first -- and the entry text rides each PR's own description instead. This is the periodic commit that applies it. `CHANGELOG.md` only, 56 insertions and no deletions: 31 entries prepended inside `## [Unreleased]` (6 under `### Added`, 2 under `### Changed`, 23 under `### Fixed`), newest merge first within each section, plus the 25 link-reference definitions they need appended at the foot of the file. Nothing already in the block was edited, reworded or reordered. Three of the 28 pull requests in the range earn no entry, each on measured precedent rather than judgement: #3150 is the previous batch pass itself, and no batch pass -- #2260, #2980, #3150 -- is cited anywhere in the file; #3130 and #3131 are routine Dependabot bumps, and none of the six merged Dependabot pull requests in the repository's history is cited either. Two pull requests carried no entry text anywhere, so theirs are written from their descriptions: #3176 (`pg_index_bloat`'s measured block rate) and #3184 (the PostgreSQL target's permissions section). * Record #3205's entry, which arrived on the merged base mid-batch #3205 merged while this batch was assembled, so `origin/dev` was merged in rather than rebased and its entry lands on top. An entry omitted because it arrived mid-batch is how a backlog restarts, and the batch's purpose is to make `[Unreleased]` current at its own merged base. One entry under `### Changed`, newest merge first ahead of #3195's, plus its link-reference definition. #3205 has no other pull request in the range behind it. * Take #3164's and #3166's entries from the lane handoff files instead of the PR bodies The entry text does not only ride the PR description. #3150's own account of the previous batch says 11 of its 66 entries came from a lane's handoff file in `~/Documents/pm-issue-queue-inbox/inbox/` rather than the body, and three files there carry dedicated entry text inside this range: `3164-changelog-entry.md`, `3166-changelog-entry.md` and `3169-changelog-entry.md`. Searching only the bodies missed the first two. - **#3164** (PR #3176) is no longer written from the PR description. Its lane wrote a full entry, and that text is used verbatim: it carries the 1,036 blocks/s upper bound, the reason the decoupling #3153 deferred is unreachable, the declined command-deadline route, the census deliberately not restated at the new ceiling, and the mutation that caught nothing. `[#2997]` joins the definition block at its ascending position, which that text cites and the file did not define. - **#3166** (PR #3168, absorbed and superseded by #3178) had no entry at all. #3178 grouped it into its own citation with a one-line summary, but its lane wrote a separate entry for the census that took `HeaviestHourlyRefreshObservedCeilingSeconds` from 594 s to 896 s and inverted the watch-line ordering. It sits beside #3178's, on #3178's merge. - **#3191's entry** said "#3184 named four of the six". #3184 was squash-merged and the four-of-six state never reached `dev`; it was a commit inside that PR, corrected before merge. "shipped" in this file means merged, so the clause now says a commit in #3184, which is what the #3187 lane's own report establishes. `CHANGELOG.md` only, 60 insertions and no deletions against `origin/dev`: 33 entries (6 Added, 3 Changed, 24 Fixed) and 27 definitions. All 15 definition runs keep the ordering they had, 9 of 15 ascending before and after. * Use the staged #3184 entry text instead of one written from the PR description #3184's entry now exists, staged by the lane that did the work, so the batch no longer invents one. Used verbatim, in `### Fixed` where its author targeted it and for the reason they gave: the section did not merely lack detail, it asserted "One role covers every collector", which is false in the direction that fails silently and which an operator would act on. Their text carries what a description-derived entry could not: the `pg_read_all_data` grant is PostgreSQL 14+ and the pre-14 fallback the section gave was `GRANT SELECT` on a schema, which is not valid PostgreSQL at all; the six collectors that need an extension, four of which additionally need `shared_preload_libraries` and a server restart; and why `pg_index_bloat` is unaffected by any grant. Both figures are live pre-change store reads rather than estimates. The citation is `[#3184]`, the pull request, because the change has no issue behind it, and its definition uses the `/pull/` form. `CHANGELOG.md` only, still 60 insertions and no deletions against `origin/dev`.
Closes #3174. Absorbs and supersedes #3168 (its four commits are merged in here, so its 896 census, its constant and its doc land with this).
The refresh grid's phase map was
index % RefreshPhaseSlots * RefreshPhaseStepMinutes, so whether two policies collided was a property of where they sat in a list: thirteen policies over four slots collide by counting alone, and which ones collided depended on order. The threecollect.query_statsconsumers sat at positions 0, 3 and 9 and were distinct only by accident. Four policies including the heaviest all started on:00.The map is injective now.
HeaviestHourlyRefreshViewis answered by identity; every other view takes its own consecutive minute in the light band. There is no modulus anywhere, so two policies cannot share a residue — thirteen policies hold thirteen distinct minutes at any list length the band can hold, in any order, with anything inserted anywhere.The derivation, as a method
The hour is three bands, and every boundary comes from a measurement or from the catalog:
LightRefreshStepMinutesLightHourlyRefreshCountHourlyRefreshPhaseOrder.Count - 1CompressionPhaseGuardMinutesceil(OtherHourlyRefreshObservedCeilingSeconds / 60)HeaviestRefreshStartMinute(LightCount - 1) * step + guardCompressionPhaseMaxPerMinuteCompressionPhaseBandMinutesceil(HypertableCount / MaxPerMinute)HeaviestRefreshWindowMinutes60 - start - bandTwo chosen inputs, everything else derived, and
MinutesInHourlyCadencecomes offHourlyRefreshScheduleSpanrather than a literal 60. The three bands are asserted to partition the hour — stronger than the oldslot * slots == cadence, which went silently short for any step that did not divide 60.The heaviest refresh's window is a remainder, not a size. Nothing in its derivation consults the ceiling it has to hold. Whether the window the hour can spare is wide enough is an assertion (
ceiling < RefreshSlotWarningSeconds), so a ceiling that outgrows the hour goes red instead of quietly re-sizing the grid around itself — and the repair is then fewer compression minutes, a cheaper refresh, or a longer cadence for that one aggregate, because there is no wider window to take.That also settles where the hour's spare minutes go without anyone choosing: the guard clears the light ceiling as surely at four minutes as at seven, and the compression band spreads the same hypertables at 3 per minute anywhere from 24 minutes wide to 27 — so the remainder lands on the only band where a minute changes an answer, at 50 s of watch-line lead time each.
Resulting grid: refreshes on
:00–:11and:15, compression on:36–:59.Why no step change was available, with a constraint nobody had named
Other * 4 < Heaviestis red at every step because it contains no step term, and at 896 s against 226.8 s the ratio is 3.95x — so both prose claims it pinned ("under a quarter of it", "more than 4x") were false. That guard is gone, not renumbered, because the ratio was never what the geometry consumed. What the geometry consumes is a chain, and it is what the tests assert now:A light refresh fits inside the guard band; the heaviest one does not. That is why one refresh gets a window no compression minute may enter and the rest are treated as occupied only for the guard band after their start.
And a seventh consequence of a wider step, not counted on #3172:
RefreshSlotPercentOfHourlyCadenceis100 / RefreshPhaseSlots, which is 25 only while the hour divides into four. Every available wider step — 20, 30, 60 — moves it to 33, 50 or 100, and V57 has already appliedDEFAULT 25toconfig.config_alert_settings.store_job_cadence_warn_percenton every live store.DarlingSelfAlertTestspins the C# seed equal to that rung's text precisely because the store column wins on a fresh store, so a moved percent means a new rung. Two independent kills on every uniform step, not one.The knob therefore stays at 25 and its derivation is broken rather than its value moved, with the reason recorded on the constant: a non-uniform grid has no single slot for a percent of cadence to name, and the tightest band is one minute. What the derivation bought is kept as an assertion — the knob fires at 900 s, inside the 1,260 s window, so it still speaks before
#3035's precondition is false. The tightness half ((pct + 1) * slots > 100) is gone with the uniform slot count it was expressed over; the knob now fires earlier than it strictly has to, which is the safe direction.The compression grid is re-derived, not renumbered
Its minutes were the second half of each uniform slot, and a grid with no uniform slot makes those minutes meaningless. The rule is unchanged — no compression minute may start while a refresh is running — and it is now one predicate over the shipped grid: a minute is clear when it is at least its own band past every refresh start, where the band is the guard for a light refresh and the whole window for the heaviest. Writing the exclude-whole decision as "its band is its whole window" keeps it a decision rather than a special case, and the walk goes through
RefreshPhaseMinutesFor, so the two cannot drift.It produces a contiguous tail band because the refreshes are contiguous too, and the band's width comes from the catalog rather than from whatever the refreshes left over — so the re-derivation moves where compression runs without changing how concentrated it is, which is the one property #3112's midnight band is sensitive to (24 minutes before, 24 after; per-minute ceiling 3 before, 3 after).
Which band opens the hour is a decision, not a layout preference. The guard is one-sided by design, so a compression policy on the hour's last minute is still holding its
AccessExclusiveLockwhen the next hour's grid opens, and whichever refresh opens the hour is the one that waits behind it. Opening with the light band puts that wait on a policy with a 226.8 s ceiling and no window to fit inside, and leaves the heaviest refresh starting a full light band clear of the previous hour's compression.The light ceiling: 226.8 s, midnight regime included
Re-derived by the read
HeaviestHourlyRefreshObservedCeilingSecondsalready names, pointed at the other twelve policies —timescaledb_information.job_history, one row per run, read at2026-09-08 14:57Z, same positional exclusion rule.Both censuses are ONE STORE'S, and that is a precondition of the read rather than a caveat about the workload (#3175).
job_historyonly records executions wheretimescaledb.enable_job_execution_loggingis on; it is off by default, and the GUC is set by the v1postgresql.confblock whose markerEnsureConfAppendedfinds already present on any pre-existing cluster — so a cluster predating the block never gains it and cannot be healed into it. Measured on two stores running the same binary: the older has the GUC absent, effectiveoff,source = default, and 1job_historyrow for 110 jobs; the newer has 39,020. A maximum over that view on an older store returns zero rows and reads as "no run exceeded the line." So neither the 594 → 778 → 896 series nor the 226.8 s figure is a fleet reading, and both are now scoped that way in the constants' own docs. #3175/#3177 has since merged and gives the GUC its own marker, so existing stores heal — which does not widen either population, because both reads predate the heal. A later census could be broader and would have to say so rather than inherit this one's scope. No shipped read touchesjob_history— every product surface usesjob_stats, deliberately — so the gap is in what an investigation can ask, not in what the product reports.This store's positive control is the census itself: 874 rows for twelve policies over three days is not what an unpopulated view looks like.
succeeded/finish filterThe two largest runs are both
query_store_stats_hourlyat00:30: 226.8 s on 2026-09-08 against 160.4 s on 2026-09-07, 41% higher night over night. The third-largest is 140.1 s, at13:45:00on the boundary day — the same reading the old constant's 140 came from, reproduced as an ordinary post-boundary member, which is what says the method is the same one rather than a new one that happens to agree.The midnight regime is IN, and the constant's own doc is why. It used to escape scrutiny on "NOTHING IS SIZED FROM IT … this figure only says how much margin that derivation happens to leave". A readout of a margin has to include the runs where the margin was consumed, or it stops being a readout of anything; removing the runs that fired a pin is the re-type-a-band-to-pass move one constant over. What the doc gains is the mechanism, and what changed underneath is that it is sized from now: the guard band is this figure rounded up to a whole minute, so the assertion became the requirement instead of a multiple of it.
The margin is the rounding, and it is 13.2 s. That is thin and it is stated rather than dressed up. What it buys is that the margin can only be consumed by the light refreshes actually getting slower, which goes red, where a multiple of a ceiling could be satisfied by a guard that had stopped covering anything. A wider guard is not free: every minute of it comes out of the window that has the measured growth series behind it.
The 874-run list is not republished — a list that long stops being re-derivable by reading and starts being a wall of digits — and that is stated as the limitation it is. What is pinned instead: the maximum against the constant, the run count against the exclusion control, the view count against the product's own
LightHourlyRefreshCount, the percentile gap against the two quantiles, the growth percentage against the two readings, the third-largest against the second, and the guard band and its rounding margin against the constant. Nine new pins, five drift-swept.Two orderings that inverted, said plainly
#3044's watch line now fires after #2136's knob, and that is forced. It was 750 s against the knob's 900 s; the window the hour can spare puts it at 1,050 s. The watch has to clear 896 s and the knob is frozen at 900 s by V57, and
896 < window * 50 < 900has no integer solution — so no geometry restores the old order while the ceiling stands where it does. The cost is real: an operator sees the cadence alert first, and #2136's documented remedy ("extend the job'sschedule_interval") is wrong for this job, because the hourly schedule interval is also itsend_offset. #3044's line still carries the correct remedy and now arrives second. Pinned in its new direction so it cannot drift back silently; not fixed here, because moving the knob needs a rung.The rejected watch-line alternative's rejection had to be re-taken, not restated. Slot less one guard band was 480 s and sat BELOW the recorded ceiling — a line under the ceiling warns on the very run the grid is sized against, and that ordering was the whole of its rejection. Shrinking the guard from half a uniform slot to the light ceiling raised it to 1,020 s, which now sits ABOVE the ceiling. Both lines clear it, they are 30 s apart, and lead time argues mildly for the lower one. What rejects it instead is coupling, which the old geometry could not have exposed: the guard is derived from a measurement of the other twelve policies, so the alternative would move the heaviest refresh's watch line whenever a light refresh got slower. Five sixths of the window depends on the window and nothing else. Both derivations are asserted, and the inversion itself is pinned.
#3168's six red tests
All six are green, and none needed anything other than geometry — every condition is the one #3168 left in place.
TheDerivedFiguresAreExactlyStateable_AndTheConstantIsThePopulationMaximumCeiling < WatchLineNoCompressionMinuteStartsWhileTheHeaviestRefreshIsStillRunningceiling < RefreshSlotWarningSeconds; the gap literalTheRefreshSlotClassifier_BandsTheLiveReadings_...Classify(ceiling) == InsideSlotInsideSlotTheRejectedWatchLineAlternative_...TheRefreshSlotReading_CarriesItsOwnVerdict_...atTheCeiling.Headroom == InsideSlotInsideSlot; 306 → 364 clear, 99.6% → 71.1%TheRefreshSlotLogLine_IsLeveledByBand_...Debug:Debug:#3168's "at least 1076 s" was off by one and is fixed to 1,077.
1076 * 5 / 6 = 896, which is not above 896, andClassifyRefreshSlotHeadroomwarns at>=. Computed rather than reasoned: the smallestswiths * 5 / 6 > 896is 1,077, and the smallest whole-minute window is 18.The latent defect
TheRefreshSlotReading_...ReportsOverrunAsNegativeHeadroomprobed overrun with a frozen literal1200and asserted-300— a bound expressed over the grid, written as a value. It fails in the worst way available: at a 20-minute geometry 1200 is the window, headroom becomes 0, and re-typing the expected figure to match would have left the case green while it had stopped testing negative headroom at all. It is derived now — one sixth of the window past the wall, the same fraction the watch line uses, so no second chosen number — with the overrun asserted strictly positive so a degenerate geometry cannot make the case vacuous.No other bound-written-as-a-literal was found in the grid. Every figure this diff touches is either a literal beside its own identity (so a frozen derivation is loud) or derived outright.
One pin of a different shape was found and answered rather than filed. The #3044 warning line names
timescaledb_information.job_historyas the route that can answer a maximum question, and a test pins that it does. That pin cannot tell an operator that following the pointer may return nothing — which is exactly what happened toget_store_metrics' description pin, green since #3119 while that route answered nothing on older clusters. The line now names the condition as well as the route, and that clause is pinned. Strictly beyond the grid; taken because the alternative is a Warning that points an operator at an empty relation, and the line's text is already being edited in this PR.What this does NOT fix
It does not fix #3112's midnight band, and nothing here should be read as claiming to. That band is the daily chunk-close burst meeting the grid at the shared midnight boundary: at every other hour the compression ticks find nothing eligible and finish in seconds, and at
00:00Zevery hypertable's newest 1-day chunk closes at once. A grid redesign changes where jobs sit, not how much work midnight carries. The re-derivation is deliberately neutral on concentration — same 24 minutes, same 3 per minute — so it neither helps nor hurts that band. Whether the chunk-close needs its own pre-midnight window is a separate question.It also does not address the trend. The heaviest refresh went 594 → 778 → 896 s over three closed days. The window buys clearance (364 s to the wall, 154 s to the watch line, against 4 s and −146 s on a 15-minute slot) and the watch line now reports something the sizing figure does not already say. It does not make the refresh cheaper.
Verification
Darling.Teststargetsnet10.0-windowsand cannot run on macOS, so the actual shipped test files —TimescaleSupportTests.cs,TimescaleContinuousAggregateTests.cs,RefreshCeilingProvenancePinTests.cs,DarlingSelfAlertTests.cs, unmodified, plus their real helpers — were compiled into anet10.0xunit.v3 suite namedDarling.Tests(soInternalsVisibleTostill applies) and run: 211 total, 0 failed, 0 errors, 17 skipped (every one live-Postgres gated), 0 not run.RefreshCeilingProvenancePinTestsreads the realTimescaleSupport.csthrough[CallerFilePath], so the prose pins ran against the shipped file. The live-Postgres and Windows-only paths are CI's to arbitrate.Red-proofed by mutation, each applied to the committed tree, confirmed applied by a
1 1numstat plus a content check, rebuilt (build failure hard-aborts the case), and restored:The rotation control is worth naming, and so is the fact that it took two goes. The contention test walks every rotation of the phase order and requires as many distinct minutes as there are policies. It first re-implemented the counting rule inline, because the public map can only be called at the one order
HourlyRefreshPhaseOrderhas — which proves the rule injective under permutation and leaves the shipped method exercised at exactly one order. That is the "a test that agrees with any derivation" failure one layer down, and it is the failure this grid exists to remove, soRefreshPhaseMinutesForgained aninternaloverload taking the order and the loop calls the shipped map against each rotated slice. Only the ORDER is a parameter — the geometry still comes from the registry, so the seam cannot fabricate a different grid, and the two overloads are pinned equal at the shipped order. Raised by review.And the control written to guard that was itself too weak.
Assert.NotEqual(unrotatedMinutes, rotatedMinutes)does not prove the rotation moved anything: an overload that ignored itsorderparameter still returns a permutation of the unrotated minutes, so the sequences differ while nothing moved — and that mutation passed. It now asserts the minute a NAMED view gets changes, and the mutation (iterateHourlyRefreshPhaseOrderinstead oforder, which is precisely the defect the inline copy embodied) reddens exactlyHourlyRefreshPhases_AreDistinctForEveryRelationTwoPoliciesContendFor.CHANGELOG
Not edited here — entry text for the coordinator to batch: