Repository navigation
Reconcile each raw hypertable's chunk interval once a day from its ingest (#4211) - #4344
Conversation
Adds RawChunkIntervalPlanner, a pure static planner (#4211): given each raw hypertable's ingest rate, current chunk_time_interval and a RAM-derived budget, it decides a target interval on a 24h/12h/6h ladder, one rung per table per day, budget-aware across every table (not only the "heavy" ones), with hysteresis on the way back up and a store-wide chunk-count cap. No database wiring yet - wiring this into the TimescaleDB ensure path and compress_after is a separate change. Per the #4211 ruling (issuecomment-5836205190): compress_after stays fixed at 1 day everywhere regardless of a table's derived interval; only chunk_time_interval moves. ChunkIntervalDays and CompressAfterDays become the ladder's ceiling rather than "the" uniform chunk width, so their doc comments, a retention test comment and RollupBackfill's #1778 slice note are reworded to match. EventWindowFloor.SkewAllowance gets its own 1-day constant instead of reading ChunkIntervalDays. A new pin asserts compress_after stays at least HourlyRefreshStartOffset. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
The #4211 ruling (issuecomment-5836734285) replaces the per-table equal share (budgetBytes / tables.Count) with a store-wide check: a table moves back up one rung only when the store-wide open-chunk total, with that move made, stays at or under half of B. With about 72 hypertables the equal share almost never let a narrowed heavy table widen again. Widening now considers the lightest-rate table first, same tie-break as narrowing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
… rung history (#4211) Checkpoint of lane I2's work, committed by the coordinator after the lane stopped. Its first full-suite run reported 4 failures and 1 test not run; they are unresolved, and a finisher lane picks them up from here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
I2's V144 (raw_chunk_interval_rung_history) added its own top-rung claim correctly, but left QueryStoreIntervalLatestRungTests.cs still claiming to be the top: RungVersion == SchemaVersion and "no reader.GetBoolean(119)". Both broke the moment V144 appended ordinal 119. Steps V143's file down the same way V141's stepped down for V142 (CollectionCaveatsRungTests): RungVersion < SchemaVersion, an atThisRung/behind boolean probe instead of all-true, and a nextArm check against V144's arm. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
… text The FleetReadsAreBoundedByTheFleet pin tied EventWindowFloor.SkewAllowance to TimescaleSupport.ChunkIntervalDays. Since #4211 decision 1 gave SkewAllowance its own 1-day constant (a clock-skew tolerance, not a chunk width), pin it to a literal TimeSpan.FromDays(1) instead so the two are free to move apart. TimescaleSupport.cs carried several comments and one log template that assumed every raw chunk is a day wide, so a table RawChunkIntervalPlanner narrows to 12h or 6h becomes eligible at every one of its own chunk boundaries, not only the shared UTC midnight the unnarrowed tables use. Reworded each to say so, without changing any scheduling behavior. Review issuecomment-5827820395 items L1-L3, ruling decision 9. L1 (RollupBackfill.SliceWidth) and the CompressAfterDays-printing log templates were already correct on this branch. L3's "7.0 GB against a predicted 6.8 GB" measurement lives only in issue #4211's body, not in RawChunkIntervalPlanner.cs or RawChunkIntervalReconciler.cs, so neither file needed a change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Item 1: RawChunkIntervalPlanner's seven Decision reasons formatted {x:N0}
with the host's culture. The rung history table now persists that text
(collect.raw_chunk_interval_rung_history.reason), so a German-locale host
would write "1.234.567 B" beside an invariant-formatted log line reading
"1,234,567 B". Wrap all seven in string.Create(CultureInfo.InvariantCulture,
$"..."). Add a fact that runs a narrowing and a widening Plan under de-DE
and asserts the exact invariant N0 string; revert-proofed by hand (dropping
the wrap on the narrowing reason turns "1,000,000 B" into "1.000.000 B" and
fails the new fact).
Item 2: ReconcileAsync's summary claimed it never throws for a planning or
apply failure on one table. There is no catch in the per-table loop, so it
does throw: that table's transaction rolls back, tables already applied
earlier in the pass stay applied and recorded, and the remaining tables and
the run row wait for the next daily pass. Reworded the doc comment to say
that, and that the caller (DarlingWorker's daily tick) catches it and logs
at Error. Comment only.
Item 3: PgMigrations' V144 summary claimed the run and history tables are
"purged by the reconciler's own caller". Grepped Darling.Storage and
Darling.Service: only SELECT/INSERT touch either table, nothing deletes
from them. Reworded to say neither table is purged, sized the growth (one
run row per daily pass, about 365 a year; one history row per move), and
noted the history has to keep at least MinimumDaysBetweenMoves days anyway
since the next pass reads MAX(changed_at) from it. Doc comment only, V144Sql
untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
|
Pushed three pre-merge fixes to this branch as one commit: Item 1: invariant-culture reason text
Added
Revert-proofed by hand: dropped the That is de-DE's dot grouping leaking into the persisted reason, exactly the bug this item fixes. Restored the wrap, rebuilt, reran: passes again. Item 2:
|
…s rung V144 -> V145 #4344 took V144 on dev. Lane B4 stopped at its turn limit mid-renumber; this commits its work as it stood (product side renumbered: V145 migration, StorageVersion 145, the probe arm, both min-schema constants; the rung test and one doc comment partly renumbered). The finishing lane completes the test side and runs the suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
The dev merge (#4344) took V144 for raw-chunk-interval-rung-history and moved this PR's wide-interval-table rung to V145. Update the leftover test references that still said V144/144 for the wide rung: QueryStoreIntervalWideWriterTests' migration lookup and doc comment, QueryStoreIntervalWideGridLiveTests' and QueryStoreTopMcpLiveTests' "V144 is present" comments, and QueryStoreIntervalWideRungTests' V144 field name (it already used the correct RungVersion=145, just under a stale name). Also fix a real regression the full suite caught: McpReadCommandTimeoutTests' receiver census flagged the new TryGetQueryStoreTopFromTableAsync's two NpgsqlCommand constructions (a literal SQL string, and transaction passed positionally) as unrecognised receivers. The census deliberately does not auto-accept a 3-arg NpgsqlCommand(sql, connection, transaction) shape anywhere in .Service, because the HypoPG experiment's monitored-TARGET command uses the identical shape. Named the literal SQL and switched both constructions to the already-recognized 2-arg NpgsqlCommand(sqlIdentifier, connection) form with Transaction set through the object initializer instead of positionally - same behavior, unambiguous receiver. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
* Add the wide Query Store interval table's write side (#3953) Lane B1: the table, writer, coverage claim, purge and schema gate for a second latest-snapshot-per-interval table that holds every outcome (Regular, Aborted, Exception) beside V143's Regular-only table, per review D4R + F1's measurement (split shape (c), ruled). B2 builds the three reads on this branch next. * Add live tests for the wide interval table, and demote V143's rung test (#3953) Lane B1: rung tests for V144 (mirroring V143's own rung file's shape), live writer tests (table-equals-raw-dedup across every outcome, a wide-only apply fault, the old-writer gap check, and 9-day retention), merged with origin/dev (no conflicts, no renumbering needed). Demotes QueryStoreIntervalLatestRungTests' "top rung" claims to QueryStoreIntervalWideRungTests, the same transformation V143 itself applied to V142's rung file when it landed. * B2: build the wide interval table's read gate and move the Queries grid onto it (#3953) QueryStoreIntervalWide.UseTable/ReadsTableAsync (Storage) decide, per the ruling (issuecomment-5836972848) and review D4R items H3/M1, whether a grid/MCP/slicer read may use collect.query_store_interval_wide instead of raw. ViewerDataService.QueryStoreTopSql is split into a shared suffix plus a raw prefix (byte-identical to before) and a new table prefix, chosen per call by the gate inside one read-only REPEATABLE READ transaction so the decision and the read see the same snapshot. * Make the table read's transaction read-only and fault-safe (#3953) TryGetQueryStoreTopQueriesFromTableAsync's REPEATABLE READ transaction was not marked READ ONLY, unlike the gate's own read intent. It also had no fault handling: an exception from opening the connection, starting the transaction, running the gate, or reading the table would surface to the caller instead of falling back to raw, unlike QueryStoreIntervalWide.ReadsTableAsync, which already catches its own faults (except cancellation) and returns the "read raw" answer. * Add the #3953 grid live pass: gate, clamp, and raw/table equality (#3953) QueryStoreIntervalWideGridLiveTests.cs runs the Queries grid's gate and table read against a real PostgreSQL store, seeded through the real write path (SeedGridAsync, reusable by a later lane for the MCP and slicer reads): several intervals, every outcome, an open interval re-fetched across three collections, and two databases. - QueryStoreTopTableSql at the gate's clamped start equals QueryStoreTopSql (EXCEPT ALL both ways, every column) for both an open end and a literal end at applied_through, and GetQueryStoreTopQueriesAsync's end-to-end rows equal raw's. - The clamp: once raw's oldest chunk is dropped (a real TimescaleDB hypertable/drop_chunks), the table read still equals raw when bound at ClampedStart, and provably does not without it. - The live gate: a pending row, a literal end before applied_through, and a sub-12-hour window each force UseTable=false on a real connection; the passing case returns true. 222 tests green: the two new classes plus every class in *QueryStoreInterval*, ViewerQueriesTests, QueryStoreTopWindowTests, DatabaseFilterTests, *QueryStoreSliceTieBreak*, ViewerSchemaVersionGateTests, StorageCommandTimeoutTests, LivePostgresCollectionHygieneTests, LiveCleanupConversionRatchetTests and DocCommentHygiene*. * B3b: move the Queries slicer onto the #3953 wide-interval table Adds QueryStoreSlicerTableSql (query_store_interval_wide) beside the unchanged QueryStoreSlicerSql, gated per ruling issuecomment-5836972848: QueryStoreIntervalWide.ReadsTableAsync called with the slicer's own raw floor (windowStart - 1 hour, since the raw statement reads from $2 - interval '1 hour'), minWindow: Zero so its bundled threshold clause never fires on the widened window, and two clauses specific to this read checked separately: QueryStoreSlicerMinWindow on the true requested window, and QueryStoreSlicerHasLegacyRowSql, a defensive gate that sends the whole read to raw if the table holds any row whose interval_start_time_utc is null (a pre-#1841 legacy snapshot) -- for such a row the table keeps only the interval's overall latest snapshot while raw's dedupe keeps the latest snapshot collected inside the window, and those can be different rows with no way for the table alone to recover the earlier one. GetQueryStoreSlicerDataAsync gains literalEndUtc (default null), and ViewerServerTab.Queries's tab-window caller passes IsCustomRange ? endUtc : null, matching the grid's existing convention. The shared ReadQueryStatsSlicerAsync and the query/procedure slicers that use it are untouched. * Move the MCP get_query_store_top read onto the #3953 wide-interval gate Splits DarlingDataReader.QueryStoreTopSql into a raw prefix, a table prefix reading query_store_interval_wide, and a shared suffix (the same shape B2 used for the viewer's grid), and wires GetQueryStoreTopAsync to try the table on one read-only REPEATABLE READ connection via QueryStoreIntervalWide.ReadsTableAsync, falling back to the unchanged raw statement on any gate refusal or fault. The web viewer's /api/read/get_query_store_top mirror reaches the same gate since it calls the same method. * B4: index the slicer's legacy-row probe, add the NULL-start live test (#3953) QueryStoreSlicerHasLegacyRowSql runs WHERE server_id = $1 AND interval_start_time_utc IS NULL on every slicer table read. ux_query_store_interval_wide doesn't carry interval_start_time_utc, so absent a matching index the probe scanned the server's whole row set in the common case where no such row exists. Add a partial index on V144's own text (server_id) WHERE interval_start_time_utc IS NULL, near-empty in steady state since the column has been populated since #1841. EXPLAIN on 50k seeded rows for one server: before, a Seq Scan reading 1250 buffers in 10.3ms; after, an Index Only Scan reading 2 buffers in 0.03ms. Add the live test B3b's brief called out but didn't write: with no NULL-start row for a server every other gate clause holds, the slicer reads the table and a table-only sinkhole row shows up; adding exactly one NULL-start row for the same server, unchanged otherwise, flips the read to raw and the sinkhole row disappears. Proved against the old shape by disabling the hasLegacyRow branch locally: the new test failed as expected, then passed again once reverted. * B4: pin the wide-table min-schema-version constants to V144 (#3953) Name the grid's and slicer's >= 144 checks (ViewerDataService.QueryStore.cs, previously bare literals) as one shared QueryStoreIntervalWideMinSchemaVersion constant, alongside B3a's own DarlingDataReader.QueryStoreTopTableMinSchemaVersion, which stays separate and untouched: that surface compares the compiled StorageVersion.SchemaVersion instead of probing the store, since the service always migrates before it serves. Add QueryStoreIntervalWideMinSchemaVersionPinTests, which finds the PgMigrations entry that creates collect.query_store_interval_wide by what it creates (not by today's version number) and asserts both constants equal it, so a renumber can't miss one. Proved by setting the viewer's constant to 145 locally: the test failed with "Expected: 144 Actual: 145" as expected, then passed again once reverted. * Finish the V144->V145 renumber and fix the MCP command census (#3953) The dev merge (#4344) took V144 for raw-chunk-interval-rung-history and moved this PR's wide-interval-table rung to V145. Update the leftover test references that still said V144/144 for the wide rung: QueryStoreIntervalWideWriterTests' migration lookup and doc comment, QueryStoreIntervalWideGridLiveTests' and QueryStoreTopMcpLiveTests' "V144 is present" comments, and QueryStoreIntervalWideRungTests' V144 field name (it already used the correct RungVersion=145, just under a stale name). Also fix a real regression the full suite caught: McpReadCommandTimeoutTests' receiver census flagged the new TryGetQueryStoreTopFromTableAsync's two NpgsqlCommand constructions (a literal SQL string, and transaction passed positionally) as unrecognised receivers. The census deliberately does not auto-accept a 3-arg NpgsqlCommand(sql, connection, transaction) shape anywhere in .Service, because the HypoPG experiment's monitored-TARGET command uses the identical shape. Named the literal SQL and switched both constructions to the already-recognized 2-arg NpgsqlCommand(sqlIdentifier, connection) form with Transaction set through the object initializer instead of positionally - same behavior, unambiguous receiver. * Fix a stacked-summary regression from the census fix (#3953) My previous commit inserted SetTransactionReadOnlySql's own doc comment between TryGetQueryStoreTopFromTableAsync's existing <summary> block and the method itself, which DocCommentHygieneTests correctly caught as two stacked summaries. Move the constant and its comment above the method's own doc block instead, so each summary sits directly over its own member. * Set #3953 wide-table thresholds from measured 12-hour cells (B4t) Ruling issuecomment-5836972848 item 3: read the table only for windows of 12 hours or more, unless the table is slower than raw at the ruled 12-hour cell, in which case that read's threshold becomes 24 hours. Measured all three reads (grid, MCP top, slicer) end-to-end through their C# read paths on rig-d4, a 15-day/3.46M-row seed at a field store's rate, median of 5 alternating runs per cell: - grid stays at 12 hours: table 1488.1 ms vs raw 1725.6 ms at 12h. - MCP top moves to 24 hours: table 737.0 ms vs raw 581.1 ms at 12h. - slicer moves to 24 hours: table 815.8 ms vs raw 595.3 ms at 12h. * Remove the Query Store slicer's table route (#4341, ruling issuecomment-5840737421) Timing showed raw beats the table at 6h/12h/24h with non-overlapping spreads, and ties at 7d, so any threshold would have made every 24h-7d slicer read slower. GetQueryStoreSlicerDataAsync now reads the raw statement at every window, unchanged from before #3953's table route existed. The grid's 12h route and MCP top's 24h route are untouched. Removed: QueryStoreSlicerTableSql, the QueryStoreSlicerRawPrefix / QueryStoreSlicerTablePrefix / QueryStoreSlicerSuffix split (folded back into one QueryStoreSlicerSql, byte-identical raw SQL), QueryStoreSlicerMinWindow, QueryStoreSlicerHasLegacyRowSql, TryGetQueryStoreSlicerDataFromTableAsync, and the now-unused literalEndUtc parameter on GetQueryStoreSlicerDataAsync and its one caller. Deleted QueryStoreIntervalWideSlicerLiveTests.cs (existed only to pin the removed table route). Lite has no twin slicer table route from #3953, so there is nothing to remove there. * Check the window before the #3953 gate's round trips (review H1, M3) H1: a short-window or literal-end-before-applied_through grid/MCP read can only ever land on raw, so check those clauses BEFORE opening the second connection, transaction, or the unindexed PlainTableFloorSql scan. The viewer and MCP top reads both check the window first; QueryStoreIntervalWide.ReadsTableAsync also short-circuits clause 4 and clause 2 before the table-floor round trip. M2 (partial, unverified grant finding reported separately): added a Trace warning to both silent fallback catches so a permission failure can't fall back to raw forever with no log. M3: cache the viewer's schema-version probe per ViewerDataService instance, consulted only after the window check. Adds a live round-trip-counting test (pg_stat_user_tables.seq_scan/idx_scan) proving a 6h grid read touches query_store_interval_wide zero times. * Drop V145's slicer-only partial index, renumber the #3953 comments to V145, restore file BOMs (#3953) * Apply the fault-injection collection to QueryStoreIntervalWideWriterTests rf2 added QueryStoreIntervalWideFaultInjectionCollection with DisableParallelization but never attached it to the writer class it was written for. Kept the existing #1776 own-store exemption comment (the class still mints its own ScratchPostgres database and does not race live-postgres), and added the missing [Collection] attribute so the class actually serializes against the other ScratchPostgres classes on this assembly's parallel pool instead of racing them. * Clean up stale grid/MCP/slicer text and the unused literalEndUtc param (M1 b-d) The slicer route to the wide table was removed by an earlier commit, but three doc comments still called the wide-table read decision 'grid/MCP/slicer' and one comment still described a partial index that was already deleted. Rewrote them to 'grid/MCP top', which is what actually reads the table now. Also removed the unused literalEndUtc parameter from GetQueryStoreSlicerDataAsync, left over from the same removed route. No caller passed it by name, so the change is source-compatible for every existing call site.
Part of #4211.
Why
RawChunkIntervalPlanner(#4332, already ondev) derives each raw hypertable's targetchunk_time_intervalfrom its own ingest rate and a RAM budget, but it is pure: no database calls, noschedule, nothing wired to a live store. Every raw hypertable still keeps the fixed interval it was
created with, so a table whose write rate grows or shrinks over time stays on a chunk size sized for its
first day. This PR is #4211's second half (lane I2, finished by I2b after I2 stopped with the work
uncommitted): it reads live ingest and catalog state, calls the planner, applies its decisions with
set_chunk_time_interval, and records what happened.What changes
Per the ruling on #4211 (issuecomment-5836205190), the five pieces:
Darling/PerformanceMonitor.Darling.Storage/RawChunkIntervalReconciler.cs. Readsevery raw hypertable's ingest rate (compressed chunks'
before_compression_total_bytes, heap and indextogether, over their wall-clock span - never the 2.29-only
hypertable_compression_statscolumns),current interval, and when it last moved (from the rung history table, so the once-a-day hold survives a
restart). Calls
RawChunkIntervalPlanner.Plan, applies every changing decision inside its owntransaction, and records the rung history plus a per-run row carrying
pg_stat_wal.wal_bytes(rulingdecision 8 / review finding L4, so a later stage can correlate WAL growth against interval moves). Runs
at startup and once a day, riding the same daily-purge tick every other maintenance chore uses; never
throws past one run - a bad reading degrades to "no changes" next time.
Darling/PerformanceMonitor.Darling.Service/DarlingConfig.cs,RawChunkIntervalReconcileEnabled(default true, file-only, takes effect on restart).Darling/PerformanceMonitor.Darling.Service/DarlingWorker.cs. Gates the whole pass on_timescaleAvailable && config.RawChunkIntervalReconcileEnabledbefore the daily-purge tick calls thereconciler, and resolves budget B:
ResolveRawChunkIntervalBudgetBytesAsyncreads the same raw RAMfigure
DarlingManagedPostgres.DeriveMemorySettingssizes Postgres from on a managed store (throughDarlingStoreHostProfile.GatherHostFacts, which itself callsDarlingManagedPostgres.TryReadWindowsPhysicalMemoryBytes- never a secondGlobalMemoryStatusExcall), and reads
shared_buffers/effective_cache_sizelive offpg_settingsviaRawChunkIntervalPlanner.BringYourOwnBudgetByteson a bring-your-own store.Darling/PerformanceMonitor.Darling.Storage/PgMigrations.csaddscollect.raw_chunk_interval_rung_history(one row per rung change: table, when, from/to hours, reason,and the three inputs the decision was made from) and
collect.raw_chunk_interval_reconcile_runs(one rowper run, whether or not it changed anything, with
wal_bytes).StorageVersion.csmoves 143 to 144, andthe probe sentinel lands in
Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.cs(
MapProbedSchemaVersion's new top arm, ordinal 119).Darling/Darling.Tests/RawChunkIntervalReconcilerLiveTests.cs(end-to-end narrow-then-holdagainst a real TimescaleDB, the bring-your-own budget path, the off-switch source check) and
Darling/Darling.Tests/RawChunkIntervalRungHistoryRungTests.cs(the V144 rung pin).I2b's own fix on top of I2's checkpoint (e45e705):
RawChunkIntervalRungHistoryRungTests.cs(V144)correctly took over the "I am the top rung" claim, but
QueryStoreIntervalLatestRungTests.cs(V143) stillasserted it -
RungVersion == SchemaVersionand "noreader.GetBoolean(119)in the viewer" - both of whichbroke the moment V144 appended ordinal 119. Stepped V143's file down the same way V141's file
(
CollectionCaveatsRungTests.cs) already stepped down when V142 landed on it:RungVersion < SchemaVersion, anatThisRung/behindboolean probe instead of all-true, and anextArmcheck againstV144's arm. This was the entire cause of the 2 failures the checkpoint's first run reported.
Pre-merge fixes
From the PR tender's pre-arm check, all in
da47a539(report: PR comment 5838942976):string.Create(CultureInfo.InvariantCulture, ...)). This PR stores the two "moved" reasons incollect.raw_chunk_interval_rung_history.reason, and{x:N0}followed the host's culture: a German Windows host would have written1.234.567 B. A planner test runs a narrowing and a widening plan underde-DEand checks for the invariant text. The test was revert-proven.ReconcileAsync's summary said it never throws for one table's failure. It does: that table's transaction rolls back, tables already applied stay applied and recorded, and the rest wait for the next daily pass. The worker's catch logs it. The comment now says that.MinimumDaysBetweenMovesdays anyway.CHANGELOG entry
Test plan
dotnet build Darling/Darling.Tests/Darling.Tests.csproj- 0 Warning(s), 0 Error(s).-classfilter, against a freshdarlingteston port 55991):*RawChunkInterval*(24),*DarlingRetention*+*TimescaleSupport*+*PgMigration*+*SchemaVersion*(145),*ViewerSchemaVersionGateTests*(32),*StorageCommandTimeoutTests*(19),*StartupCommandTimeoutTests*(14),*LivePostgresCollectionHygieneTests*(11),*LiveCleanupConversionRatchetTests*(15),*McpPayloadContractCensusTests*(68),*DocCommentHygiene*(77). All 0 failed on the first pass, beforeorigin/devwas merged in.IntervalLastChangedUtcto always benullinstead of readingit from the history table, rebuilt, ran
EndToEnd_NarrowsOneRungOverBudget_ThenHoldsForOneDayFromHistory_AndMigrationReapplyIsANoOp_AgainstDevPostgresalone - phase 2 (the same-day hold) failed (
Expected: 0, Actual: 1, meaning it moved a second timeinstead of holding). Restored the read, rebuilt, reran - passed.
DarlingWorker.ResolveRawChunkIntervalBudgetBytesAsyncon a managed store callsDarlingStoreHostProfile.GatherHostFacts(), which callsDarlingManagedPostgres.TryReadWindowsPhysicalMemoryBytesdirectly (DarlingStoreHostProfile.cs:264).The startup sizing path (
DarlingManagedPostgres.GetTotalPhysicalMemoryBytes/TryGetAuthoritativePhysicalMemoryBytes, which feedsDeriveMemorySettings) calls the sameTryReadWindowsPhysicalMemoryBytes(DarlingManagedPostgres.cs:3331). Same read, not a secondGlobalMemoryStatusExcall. On a bring-your-own store,BringYourOwnBudgetBytes_ReadFromPgSettings_AgainstDevPostgresalready exercises the live
pg_settingspath. No code change was needed for this check.origin/dev(eac444e, PostgreSQL-target baselines PR) - clean merge, no conflicts.devisstill at schema version 143, so V144 needed no renumbering.
Darling.Testssuite, run against a freshly recreateddarlingtest, twice:48, Not Run: 1, Time: 826.060s. Both failures were
QueryStoreIntervalLatestRungTests.TheRungIsRegisteredAtTheTopOfADenseLadderand.TheProbeMapsAFullyMigratedStoreToThisTopRung- fixed as described above.Skipped: 48, Not Run: 1, Time: 968.524s.
[Fact(Explicit = true)]tests in the whole suite(
McpSchemaCompatServiceLeakRaceTests- an intentionally-manual race repro for Lite budget test sometimes sees get_alert_history's dataService parameter in a full run #4075 - andRollupCoverageRoutingTests- marked explicit pending LA-3b), neither of which this PR touches.a second UTC-logging target,
DARLING_TEST_PGRUNTIME, etc.) - the standard shape for this suite.Lite.Tests- not run. This PR touches only Darling's own PostgreSQL/TimescaleDB store; Lite has noPostgreSQL storage layer, so there is no Lite parity surface for a chunk-interval rule.
Installer.Tests- never run per lane orders.Not done
Per the brief, these are left for lane I3:
TimescaleSupport.ChunkIntervalDaysrather than a restated literal (RawChunkIntervalPlanner.cs:69-71already does this), but the brief calls out that the regression pin for it, plus some stale text, are
still owed.
and are not addressed here.
For the coordinator
C:\GitHub\worktrees\rig-i2) was reused per the brief and has now been stopped andthe directory removed.
reconciler, the off switch, the wiring, the migration, budget sourcing) matched the ruling and needed no
changes.
I3 (decision 9 pins and stale text)
Branch: pushed on top of I2b's commit (
072cf2b6->117cb49f). Build: 0 Warning(s), 0 Error(s). Targetedclasses (
*FleetReadsAreBoundedByTheFleet*,*TimescaleSupport*,*RawChunkInterval*,*DarlingRetention*,StartupCommandTimeoutTests,DocCommentHygiene*): 237 total, 0 failed, 21 skipped (live Postgres tests, norig per the brief).
DocCommentHygieneTestsrun alone afterward: 77/77 passed (it catches a broken<see cref>, among other things).The pin.
FleetReadsAreBoundedByTheFleetTests.cs'sEventWindowFloor_IsOneChunkBeforeTheWindow_AsNaiveUtc(~line 119) asserted
TimeSpan.FromDays(TimescaleSupport.ChunkIntervalDays) == EventWindowFloor.SkewAllowance.Now asserts
TimeSpan.FromDays(1)directly, with a comment saying why: since #4211 ruling decision 1,SkewAllowanceis its own clock-skew tolerance, not the raw chunk width, so a tableRawChunkIntervalPlannernarrows must be free to move itschunk_time_intervalwithout dragging this pinalong.
Stale text in
TimescaleSupport.cs. Reworded 7 spots - all comments or one log template, no behaviorchange:
CompressScheduleInterval's doc (~line 90): "12 hours [is] the default on EVERY store shape" was wrongfor a narrowed table - TimescaleDB's own uncapped default for a 12h or 6h chunk would compute 6h or 3h, not
12h. Scoped the claim to day-or-wider chunks and named the narrowed-table exception.
CompressionPhaseMaxPerMinute's doc (~line 4559): "every hypertable's newest 1-day chunk becomes eligibleat the same UTC midnight" is still literally true - every ladder rung (1-day, 12h, 6h) divides a day
evenly, so midnight stays a shared boundary - but it was incomplete: a narrowed table also picks up
boundaries of its own. Reworded to say both.
ChunkIntervalDays are both 1" framing, now stale since only
CompressAfterDaysstays fixed at 1 day.Reworded, and noted a narrowed table's extra boundaries argue for more spread, not less.
nothing eligible" no longer holds for a narrowed table, which has real work at 2 or 4 hours instead of 1.
Scoped to "a table still at the 1-day chunk ceiling", with a "fewer for a narrowed table" clause.
AggregateCompressionBandFirstHour's doc (~line 6938): "the 00: hour's compression band is the one thatdoes a day's compressing" implied that is the only heavy hour; a narrowed table also does real work at
noon (12h chunks) or every 6 hours. Reworded to say the 00: hour is the one every raw table shares, and
named the extra hours a narrowed table adds that this constant does not try to clear. Raw and aggregate
compression lock different chunks per the paragraph directly above this one, so this reads as a
resource-contention question rather than a lock-safety gap - flagging it for the coordinator in case it is
worth a closer look, not as a bug I found evidence of.
RefreshOverrunlog template (~line 10662): "every hypertable's newest {Days}d chunk becomes eligibleat the same UTC midnight, so one tick a day carries a full day's rewrite while the other twenty-three find
nothing" printed
CompressAfterDays(which stays 1 day, correctly, per the brief's own rule), but thesurrounding prose claimed a universal once-daily pattern. Reworded to describe the delay-after-close
mechanism generally, scoped "one tick a day" to the 1-day ceiling, and named what a narrowed table does
instead.
CompressionActivity.AssignedPhaseMinute's doc (~line 11205): referenced "the daily chunk close of a1-day raw chunk" to explain why the log in Lite overview: show Online/Offline status #6 does not fit an aggregate job. Reworded to match Lite overview: show Online/Offline status #6's new
framing.
Checked, left alone:
RollupBackfill.SliceWidth's Compression waits for the next fixed tick, so a just-closed chunk can sit uncompressed most of a day (81 GB observed); deadlock correlation to watch #1778 note) and theEventWindowFloor.SkewAllowanceown-constant, theCompressAfterDays_IsAtLeastTheHourlyRefreshStartOffsetpin, and theDarlingRetentionTestsceiling notethe brief named - all already correct on this branch (Derive each raw hypertable's chunk interval from ingest (#4211) #4332).
CompressAfterDays(~lines 8187, 8908, 10586, and the#1778parentheticalat ~8622) - left as is, because
A(the compress delay) stays fixed at 1 day on every table regardless ofnarrowing, so those read correctly as written.
RawChunkIntervalPlanner.csandRawChunkIntervalReconciler.csin full, plus the wiring inDarlingWorker.cs/DarlingConfig.cs, andgrepped the whole repo for both figures. They appear only in issue Design: size raw chunk interval, compress_after and the hourly CAGG refresh window from ingest rate; a large store keeps 1–2 days of raw uncompressed and it outgrows RAM #4211's own body (the design writeup),
not in any code comment. Neither file claims the rate model was validated more widely than that one reading,
so nothing needed rewording there.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ