You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Retention policies run their first check immediately — no external window to pause them, caused permanent data loss #1680
Field finding from prod-sql-use2-monitor-01 during the tiered-retention deploy on 2026-07-24. This one caused real, permanent data loss and should be fixed before 3.3.0, because 3.3.0 is exactly when existing stores get these policies for the first time.
What happens
TimescaleDB runs a newly-created retention policy's first check immediately at job creation, not on its next scheduled interval. Confirmed on the field box via job_stats timestamps: all 6 new retention jobs fired and completed within the same minute as service startup (00:50:05-00:51:02 UTC) — before any external SQL session could pause them per the runbook's intended pause-before-backfill ordering.
Result on that store: ~2 days of query_stats/procedure_stats history and all of query_store_stats history were dropped before the pause could take effect. Not recoverable.
There is no external window to win this race. Being faster is not the fix.
No scheduled argument anywhere in the file — so every policy is created live and armed.
Fix
Create the job already paused in the same call that creates it, then enable it only after a verified backfill:
add_retention_policy(..., scheduled => false)
then alter_job(<id>, scheduled => true) once the CAGGs are confirmed backfilled past their horizons
That closes the race by construction rather than by timing.
Why this matters more now than it did last week
The cold-start caveat already documented on EnsureRetentionPoliciesAsync — that an existing store must have its CAGGs backfilled past the horizon before retention runs — is currently only enforceable by an operator racing the scheduler and losing. Any 3.3.0 upgrade of a store with history to preserve hits the same window.
Note also that #1664 added a seventh policy (query_stats_db_hourly), so the fix should cover the whole list rather than the original six.
Fixed by #1711 (merged to dev), which supersedes #1684's approach.
The race is closed atomically rather than by being faster: add_retention_policy and alter_job(job_id, scheduled => false) now run in the SAME transaction. The TimescaleDB job scheduler is a separate backend, so MVCC hides the uncommitted bgw_job row from it entirely — the job becomes visible only at commit, at which point it already reads scheduled = false. There is no window in which an armed policy exists, so no external session has to win a race. TimescaleSupport.PauseJobSql's doc comment cites this issue for exactly that reason.
Note for the record: #1684 tried to do this with a scheduled argument to add_retention_policy, which does not exist on any 2.x — that became #1705.
Activity
Merge pull request #1684 from erikdarlingdata/feature/1680-retention-…
erikdarlingdata commentedon Jul 27, 2026
Fixed by #1711 (merged to dev), which supersedes #1684's approach.
The race is closed atomically rather than by being faster:
add_retention_policyandalter_job(job_id, scheduled => false)now run in the SAME transaction. The TimescaleDB job scheduler is a separate backend, so MVCC hides the uncommittedbgw_jobrow from it entirely — the job becomes visible only at commit, at which point it already readsscheduled = false. There is no window in which an armed policy exists, so no external session has to win a race.TimescaleSupport.PauseJobSql's doc comment cites this issue for exactly that reason.Note for the record: #1684 tried to do this with a
scheduledargument toadd_retention_policy, which does not exist on any 2.x — that became #1705.Fix both BLOCKING defects: estimator units, and the interrupted-backf…
Merge pull request #1793 from erikdarlingdata/feature/1784-coverage-c…
Stand the job scheduler down before a scratch database is force-dropped
Create retention policies paused, arm only when provably safe (erikda…
Retention policies were never created at all, on every store (erikdar…
Phase 1 (erikdarlingdata#1759): route by rollup COVERAGE, not age alone
Phase 2 (erikdarlingdata#1759): --backfill-rollups, a preflighted ope…
Make the catalog sweep honor the erikdarlingdata#1680 coverage gate (e…
Converge each rollup to its SOURCE, not to raw (erikdarlingdata#1798)
Correct the Query Store rollups: dedup cumulative interval snapshots …
Make retention held by the coverage gate visible (Fixes erikdarlingda…
RawRetentionApplies is scoped to the grain, because the arming gate is
The duration-trend trio routes by retention tier and discloses its so…
4299 L3a: redirect the two census reads and the stuck WARNING text fo…