Repository navigation
Take raw query-statistics tables out of the catalog sweep's drop path (#4427) - #4429
Merged
erikdarlingdata merged 4 commits intoSep 26, 2026
Merged
Conversation
…#4427) The daily retention sweep (DarlingRetention.PurgeAsync) gated every collector table's drop_chunks on a floor-only coverage check (IsTieredDropSafeAsync -> TimescaleSupport.IsRawTierDropSafeAsync), comparing each rollup's oldest bucket against raw's oldest row. A hole inside the covered span -- an outage seam the repair hasn't reached, a range the repair deferred, a failed repair -- moves neither floor, so the sweep read Covered and dropped the chunk the service-triggered, fully-gated raw purge is holding for exactly that reason. On a TimescaleDB store, query_stats, procedure_stats and query_store_stats (membership from TimescaleSupport.RawRelations) now leave the sweep's drop path entirely; the service-triggered purge already owns them with the full gate (fresh coverage, repair epoch, resolvable successor, clean interior-hole scan). Neither purged nor failed -- one Information line names the owner. Plain-PostgreSQL mode is unchanged: no rollups there, so its DELETE fallback stays raw's only purge. purge_now now runs the same gated trigger once on a pooled connection, then reports each raw relation's outcome (ran, not_covered, hole, epoch_stale, gate_unknown, gate_error, no_chunks, run_failed) with a plain note, including a note when a shorter custom retention was requested but could not apply to these three tables. Adds RawTablesLeaveCatalogSweepLiveTests.cs: a live pin proving an interior hole's raw rows and chunk survive the sweep (RED on dev at runtime -- Assert.Equal(24, 0) on the un-fixed code), a plain-PostgreSQL DELETE-fallback pin, and a purge_now report pin. A mutation removing the RawRelations membership check reproduces the same RED.
…the raw step fails, and pin the new sweep contract (#4427)
…ort on live stores (#4427)
erikdarlingdata
marked this pull request as ready for review
September 26, 2026 16:15
erikdarlingdata
deleted the
fix/4427-raw-tables-leave-the-catalog-sweep
branch
September 26, 2026 16:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #4427.
Why
The daily retention sweep (
DarlingRetention.PurgeAsync) gated every collector table's Timescale drop on a floor-only coverage check (IsTieredDropSafeAsync→TimescaleSupport.IsRawTierDropSafeAsync), which compares each rollup's oldest bucket against raw's oldest row. A hole inside the covered span — an outage seam the repair hasn't reached, a range the repair deferred, a failed repair — moves neither floor, so the sweep read Covered and dropped the chunk the service-triggered, fully-gated raw purge is holding for exactly that reason. The gate held precisely when a hole existed; the sweep, on a shorter horizon (operator-controllable down to 1 day), defeated it.What changes
On a TimescaleDB store,
collect.query_stats,collect.procedure_statsandcollect.query_store_stats(membership fromTimescaleSupport.RawRelationsitself, never a copied list) now leave the sweep's drop path entirely. They are counted neither as purged nor as failed — the sweep logs one Information line naming the owner and moves on. The gated, service-triggered purge (DarlingWorker.TriggerRawPurgeCoreAsync) already owns them with the full gate: a fresh Covered verdict, a current repair epoch, a resolvable successor, and a clean interior-hole scan.The skip is decided by whether the TimescaleDB extension is installed in the store (a
pg_extensionprobe on each sweep), not by the service's cachedtimescaleAvailableflag. A stale flag therefore cannot reopen the drop path. If the probe itself fails, the raw tables are kept that pass and one Warning is logged.True plain-PostgreSQL mode (no extension row) is unchanged: there are no rollups there, so the DELETE fallback stays raw's only purge for these tables.
purge_nownow runs the same gated trigger once, on a pooled connection, right after the sweep (when Timescale is available), then reads back each raw relation's outcome and adds arawTablesarray to the JSON response:relation,outcome(ran,not_covered,hole,epoch_stale,gate_unknown,gate_error,no_chunks,run_failed) and a plain-languagenote. When the requested custom retention is shorter than the gated horizon (4 days), every raw entry's note says plainly that the custom value was not applied to it. The gate is never bypassed.purge_nowreads the database clock before the trigger runs. A stored outcome older than that is reported asgate_unknown("no outcome was recorded for this pass"), never as this pass's result.If the raw step fails (connection or trigger),
purge_nowstill returns the sweep summary, withrawTablescarryinggate_errorand the message. A shutdown still propagates.The report's horizon note comes from
TimescaleSupport.RawRetentionSpan, and a custom retention longer than it gets a note that it doesn't apply to raw tables.Every raw drop path
DarlingRetention.PurgeAsync)DarlingRetention.cs:331(new skip)DarlingRetention.cs:~370(PurgeOneAsync)DarlingWorker.cs:7269TriggerRawPurgeCoreAsync→CALL run_job(id)(TimescaleSupport.cs:6355)purge_nowDarlingWorker.cs:8576RunPurgeNowAsyncConvergeRawArmedStateSql) — raw jobs never run on Timescale's own cron; only the service-triggeredCALL run_jobabove fires themDarlingWorker.cs:7177(ReevaluateRetentionPoliciesAsync)TriggerRawPurgeAsync) — no separate drop pathTimescaleSupport.csRetentionPoliciesDarlingCliCommands.cs)QueryStoreSliceRepair.cs:271DELETE FROM collect.query_store_statsthat removes duplicate rows and reinserts one per slice; not retention, and no rows are lostNo other floor-only raw drop path was found.
Test plan
New file
Darling/Darling.Tests/RawTablesLeaveCatalogSweepLiveTests.cs, own-store (ScratchPostgres, not[Collection("live-postgres")]):InteriorHole_SurvivesSweep_RawTableLeavesDropPath— seedsquery_statsover 10 days, refreshes every successor EXCEPT a 1-day interior window (a real hole well inside the covered span). Confirms the floor-only check (IsRawTierDropSafeAsync) readstruedespite the hole, then runsPurgeAsync(timescaleAvailable: true, retentionDaysFor: _ => 1)and asserts the hole's 24 raw rows and its chunk count are unchanged.origin/devat runtime:Assert.Equal() Failure: Expected: 24, Actual: 0atRawTablesLeaveCatalogSweepLiveTests.cs:200— dev's sweep drops the hole's chunk because the floor-only gate is the only check it runs forquery_stats.PlainPostgres_StillDeletesExpiredRawRows— plain-PostgreSQL mode, confirms the DELETE fallback still purges expired raw rows. GREEN on both dev and this branch (unchanged behavior).PurgeNow_RawTableReport_ShowsHoleAndCustomRetentionNote— drives the same hole scenario throughTriggerRawPurgeCoreAsyncthenDarlingWorker.BuildRawTablePurgeNowReportAsync, asserting the raw relation's outcome isholeand the note mentions the custom-retention caveat.Mutation: commented the
RawRelations.Containscheck out (timescaleAvailable && false && ...), rebuilt, reranInteriorHole_SurvivesSweep_RawTableLeavesDropPath— same failure,Assert.Equal() Failure: Expected: 24, Actual: 0at the same line. Restored the check, rebuilt, reran — GREEN (3/3 in the new class).StaleTimescaleAvailableLatch_DoesNotReopenDropPath: the extension is installed butPurgeAsyncgetstimescaleAvailable: false, with a short retention over an interior hole. The hole's rows survive.timescaleAvailableargument again (the previous head's logic) fails it withAssert.Equal() Failure: Expected: 24, Actual: 0. Restored, it passes.ProbeRawSkipSafe_ConnectionFailure_KeepsRowsAndLogsOnce: a probe that can't connect keeps the raw tables and logs exactly one Warning.PlainPostgres_StillDeletesExpiredRawRowsnow drops the extension from its scratch database first. The TimescaleDB image'stemplate1carries it, so the test previously never exercised true plain-PostgreSQL mode.PurgeNow_RawTableReport_StaleOutcome_ReportsGateUnknown: a realranrecord older than the pass start is reported asgate_unknown.RawPurgeNowWiringTests:RunPurgeNowAsynccalls both the gated trigger and the report builder, and the response carriesrawTables.PayloadDimensionLiveTests: its two catalog-sweep tests now pin the new contract. Raw rows survive the sweep whether or not the rollup covers them, and dimension content they reference is kept.Latest run against a local TimescaleDB 2.30.1/PG18 container:
RawTablesLeaveCatalogSweepLiveTests6/6. Together withPayloadDimensionLiveTests,RawPurgeNowWiringTests,DarlingRetentionTests,RawPurgeTriggerLiveTestsandDocCommentHygieneTests: 153 passed, 0 failed.Earlier run, before the probe and report changes:
RawTablesLeaveCatalogSweepLiveTests: 3/3 pass.DarlingRetentionTests: 43/43 pass.ScheduledPurgeLaunchTests: 5/5 pass.RawPurgeTriggerLiveTests: 8/8 pass.AlertReadFailureSurfaceTests: 27/27 pass.DocCommentHygieneTests: 77/77 pass.DarlingCommandExecutorTests: 32/32 pass.Darling.Tests.csprojbuild (-c Release -p:EnableWindowsTargeting=true): 0 errors, 0 warnings.Lite.Tests.csprojbuild (-p:EnableWindowsTargeting=true): 0 errors, 0 warnings.Not run locally: the full suite.
Darling.Tests/Lite.Teststargetnet10.0-windowsand cannot execute their full run on macOS either way; CI decides the rest.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF: [Take raw query-statistics tables out of the catalog sweep's drop path (#4427) #4429]: Take raw query-statistics tables out of the catalog sweep's drop path (#4427) #4429