Repository navigation
The startup sweeps stop a continuous aggregate's jobs before they drop it - #5552
Merged
Merged
Conversation
…p it (#5551) Stop the aggregate's jobs, wait out a worker already running, then drop. At the cap the jobs are scheduled again and the drop is skipped; a failed drop schedules them again after the connection reopen. The reshape sweep also reopens its connection after a failed drop.
…ed delay, tag the pin class as a guard (#5551)
…stopped jobs, tell the truth about the scheduler (#5551) Review round 1 on the quiesce: - the live hold test now fails without the wait (the sweeper's backend must never run the DROP while the worker is alive) and skips, saying so, when the scheduler ended the worker before the hold was observed - the cap test uses a zero cap so the skip path runs before the scheduler can end the worker - the stop is inside the try; every resume runs on its own 10 s grace token; the cap skips only when the final look still lists a worker - the log names the stopped job ids and the view when the connection cannot be reopened or the jobs cannot be scheduled again - docs and log text no longer say no backend is ended: stopping a job makes the scheduler end its running worker, which records one failed run
erikdarlingdata
marked this pull request as ready for review
October 8, 2026 11:00
…the stopped jobs and ends the sweep (#5551) The caller's reopen after a failed drop runs on the 10 s resume grace. Its OperationCanceledException escaped the per-relation catch while the caller's token was live, leaving the jobs stopped with no log line and, at startup, stopping the service. It is now caught: the stopped job ids and the view are logged, the sweep ends like any failed reopen, and it is rethrown only when the caller's own token is cancelled. Also fixes a hand-rolled comment filter in the quiesce pin test that CommentFilterAdoptionTests flags.
8 of 9 tasks
…eProbe (#5551) TimescaleEnableConnectionSafetyTests flags a TryEnableAsync result that is skipped on rather than asserted on the spot, and (#1922) CREATE EXTENSION can kill the backend it runs on. OpenStoreAsync now enables on the probe's own connection after migrating, and disposes the connection it opened when the test skips instead of leaking it.
erikdarlingdata
added a commit
that referenced
this pull request
Oct 8, 2026
Adds the changelog entries for the 3.10.0 PRs that merged after the first splice: #5528, #5532, #5537, #5542, #5543, #5552 and #5553. Each full entry goes into docs/changelog/3.10.md and its title-only line into the CHANGELOG.md index. The archive census row for 3.10.0 moves from 257 to 265 entries. Changelog entries for the 3.10.0 PRs merged after the first splice.
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.
Fixes #5551.
Why
Three startup sweeps drop a continuous aggregate with
DROP MATERIALIZED VIEW ... CASCADEwithout stopping its jobs or waiting for a refresh that is already running. A refresh running during the drop gaveXX000: tuple concurrently deleted(#5416, already caught and recovered). It is the inferred cause of a PostgreSQL backend crash in CI (#5549).What changes
Before each drop of a continuous aggregate,
QuiesceContinuousAggregateJobsAsync(inTimescaleSupport.cs):timescaledb_information.jobs, joined tocontinuous_aggregateson either identity a job can report (the user view or the materialization hypertable, the joinContinuousAggregateRefreshStateSqlalready uses). The view name is a bound parameter. It also follows aggregates built on the one being dropped (the daily tier), because the CASCADE drops those and a refresh of the daily tier is the same race. Calls are unqualifiedalter_job, like the rest of the product code.alter_job(job_id, scheduled => false), one statement) and remembers which.pg_stat_activity(this database,backend_typeending in[<job_id>]) show no worker of any of the aggregate's jobs, stopped or not.ReopenBrokenConnectionAsync. A cancellation (shutdown, or the hourly pass's budget) schedules them again too. Every resume runs on its own 10 second grace token, never the caller's.timescaledb_information), a plain view and an absent relation have no jobs, so this is a no-op, as before.The sweep itself cancels and terminates no backend, but stopping a job makes TimescaleDB's scheduler end that job's running worker (see Review round 1, finding 7).
A job that was already stopped before the sweep is never in the remembered set, so it stays stopped.
Sites (every
DROP MATERIALIZED VIEWin product code)git grep -n "DROP MATERIALIZED VIEW" -- Darling/finds two product statements, both inTimescaleSupport.cs, covering three call sites. All three now quiesce:DropRetiredBaselineAggregatesAsync, superseded pass (Brains-review campaign: deferred structural residue (from #3538 / #3539 / #3540 / #3541) #3653), throughDropRetiredBaselineRelationSql.DropRetiredBaselineAggregatesAsync, retired pass (Retire the orphaned cpu_utilization_baseline / file_io_baseline continuous aggregates (#1995 cleanup) #2007), throughDropRetiredBaselineRelationSql.DropStaleContinuousAggregatesAsync, the composer-dimension reshape (four views, CASCADE takes the daily tier).The rest of the hits are test code or comments. A census test (below) fails if a new product drop appears.
The cap: 30 seconds, polled every 250 ms
Both sweeps run on the start path (serially, before the collectors start) and again on the hourly store-maintenance pass, whose whole budget for every step is 5 minutes. A drop happens once per store. Because the jobs are stopped first, only a refresh already running is waited for, so the wait is a few seconds for these small or day-old aggregates: TimescaleDB's scheduler usually ends the worker itself within about 3-4 s of the stop (measured), and a worker that finishes first is just as good. Worst case, every one of the eight relations (two superseded, two retired, four reshaped) is stuck behind a worker: 8 x 30 s = 4 minutes, still inside the hourly 5. A longer cap would let a stuck worker stall startup or eat the hourly budget; a refresh that really takes longer costs nothing to wait out, because the aggregate is skipped and retried, not dropped under a worker. A pin test holds the number and the 8 x cap <= 5 minutes arithmetic.
Small defects fixed in code I touched
DropStaleContinuousAggregatesAsynchad no connection reopen after a failed drop. AnXX000closes the Npgsql connection (Flaky test: the retired baseline sweep test finds its connection closed after the first attempt #5416), so the next view's stale check threw "Connection is not open" and logged a second, misleading failure. It reopens now, like the retired-baseline sweep.Both sweeps gained an overload that takes
AggregateJobQuiesceOptionsbefore the token (the production callers and signatures are unchanged); the tests use it to reach the cap in 2 seconds.Review round 1
RetiredBaselineSweepRecoveryTestssweeper's search path): fixed in Darling tests: a scratch database drop no longer hands its backend to the next test, and the retired-baseline race no longer runs a refresh beside the drop #5550, which owns that test.pg_stat_activityfor the DROP while the same worker pid is alive on both sides of each look, requires at least one look that saw the hold, and skips (with the reason) when the scheduler ended the worker first. RED-checked by deleting the wait loop: (b) fails.alter_jobstatement. The three catches share one helper for the reopen and the resume.alter_job(scheduled => false)the scheduler ended the worker about 3-4 s later; the server log showsjob N threw an errorand an ERROR (invalid transaction termination) from the worker, and the job's stats went tototal_failures+ 1 withlast_run_status= Failed (job_statusPaused while stopped).timescaledb_information.job_statsrow and the rawbgw_job_statrow are all gone with the aggregate (measured: 0 rows after the DROP), so the arm never sees it. After a skip or a failed drop: the job stays with one more failure and Failed as its last status until its next run succeeds, which is exactly the arm's condition (failures grew since the previous hourly sample AND last run Failed), so it can fire once at INFORMATION ("N new failure(s)", cooldown applies) if its hourly sample lands before the job's next successful run. It needs a worker actually running at the moment of the stop AND a kept aggregate, so it is rare. I did not change the arm: telling this failure from a real one would need the arm to know about the sweep, which is a design decision, not a small fix. I documented it on the arm and on the quiesce. Options for the seat: leave it (INFORMATION, rare, truthful: a run did fail), or have the resume path run the job once right after resuming so its last status becomes Success.Review round 2
Finding 1 (Medium, from round 1's own change): the grace-token reopen could throw out of the sweeps' catch. After a failed drop that holds stopped jobs,
ReopenAndResumeAfterFailedDropAsyncreopens the connection on the 10 s resume grace.ReopenBrokenConnectionAsyncdoes not catchOperationCanceledException, so a reopen that took longer than the grace (a pool wait, or a host that stopped answering; Npgsql's connect timeout is 15 s) threw it past the per-relation catch while the caller's token was still live. The jobs stayed stopped with no log line, the hourly pass skipped every later convergence step, and at startup the exception reached the host and stopped the service.Fix: the grace reopen is now inside a
trythat catchesOperationCanceledException. It logs the reopen-failed template plus the stopped job ids and the view ("the connection did not reopen within 10 s"), and returns false, which ends the sweep like any failed reopen. It rethrows only when the caller's own token is cancelled, after logging. The reschedule that follows cannot throw past the caller:ResumeContinuousAggregateJobsAsynccatches everything, including its own grace running out. The helper is nowinternalwith an optionalresumeGraceargument as the test seam (production leaves it unset).Audit of every other use of the grace token and every exit of the quiesce and resume paths. Only this one escaped.
s_resumeGraceis used in two places: the resume (reopen and reschedule both inside acatch (Exception), logged with the job ids) and this reopen (now fixed).QuiesceContinuousAggregateJobsAsync: its only grace use is the resume in its catch-all and in the cap branch, which cannot throw, so the original exception is what the caller sees.catch (OperationCanceledException) when (hold.Unscheduled.Count > 0)blocks resume on the grace (cannot throw) and rethrow the caller's own cancellation. A cancellation from the quiesce comes only from the caller's token (the grace is never passed to it), and the hold is empty then, so those blocks are not entered a second time.Tests: new
AggregateJobGraceReopenTests(no database; a never-opened connection and a zero grace, so the reopen's token is cancelled before it starts). One test pins that a grace-expired reopen returns false and logs the job ids, the view and thealter_jobstatement, with one warning. The other pins that with the caller's token also cancelled it logs the jobs and then throws. A source pin was added to the quiesce pin class (the reopen sits in that try/catch with the caller-token rethrow, and there are exactly two grace tokens).RED-check: with the catch disabled (
when (false)), both new tests FAIL (the OperationCanceledException escapes, or the log line is missing). With the fix, both pass.Also fixed (a CI failure on this PR):
EveryProductDropOfAContinuousAggregate_IsPrecededByTheQuiescefiltered comment lines withStartsWith("//"), whichCommentFilterAdoptionTests.EveryLinePrefixCommentFilter_StatesTheBoundItRestsOnflags as a hand-rolled comment filter (the Guard tests job was red). It now uses an anchored regex for a whole-line comment.Run for this round (rig PostgreSQL 18.6 + TimescaleDB 2.30.1 on port 55512, UTC, CI's worker settings; stopped by PID afterwards): build 0 warnings;
AggregateJobQuiesce*(pins and 8 live) plusAggregateJobGraceReopenTests,RetiredBaselineAggregate*andStoreObjectConvergence*: 37/37; everyStage=Guardtest: 1401/1401 (this includes the comment-filter census);*Census*,*Ratchet*,*Hygiene*,*Pin*,*Guard*,*StartupCommandTimeout*,*StorageCommandTimeout*: 1531 run, 0 failed, 2 skipped (pre-existing skips). Full suite not run locally.Also fixed (a second CI failure on this PR, Darling PG shard 5):
TimescaleEnableConnectionSafetyTests.NoLiveTest_KeepsUsingAConnectionItHandedToTryEnableAsyncflaggedAggregateJobQuiesceTests.cs:676:Assert.SkipWhen(!await TimescaleSupport.TryEnableAsync(connection, null, ct), ...). The census accepts only an inlineAssert.True(await ...), a result captured and checked in the very next statement, or a route throughLiveTimescaleProbe.TryEnableAsync(LivePostgresStoreFixture keeps using the connection after CREATE EXTENSION timescaledb kills the backend, masking the real cause #1922:CREATE EXTENSIONcan kill the backend it runs on). That was the file's onlyTryEnableAsynccall.OpenStoreAsyncnow migrates the store, then enables TimescaleDB throughLiveTimescaleProbe.TryEnableAsyncon the probe's own connection and skips when it is unavailable, so the skip-without-TimescaleDB behaviour is kept. Small defect fixed in the same method: a skip (or a failed setup) used to leak the connection it had opened, because the caller never received it to dispose; it is disposed on that path now. The census is untouched. Earlier lanes ran only the Stage=Guard classes and missed this one; this time I ran every source-scanning class (any class in a file that usesEnumerateFiles,GetFiles(...*.cs)orFile.ReadAllText) plus every class named Census, Ratchet, Hygiene, Guard or Pin: 580 classes in 6 batches, 8863 tests, 0 failed exceptSensitiveStatementAutoParamTests.Measure_20MbPlan_WithACut_ThreeShapes_StayUnderTheCeilings, a timing-ceiling test that reads no source and passed alone (44 of 44) on re-run.TimescaleEnableConnectionSafetyTestsis 2 of 2. Live:AggregateJobQuiescePinTests,AggregateJobGraceReopenTestsandAggregateJobQuiesceLiveTestsagainst a throwaway PostgreSQL (TimescaleDB preloaded, UTC, port 55513, stopped afterwards): 17 of 17, none skipped. Build 0 warnings. No full suite run locally; CI decides.Test plan
Rig: PostgreSQL 18.6 + TimescaleDB 2.30.1 on port 55510, UTC, CI's worker settings.
AggregateJobQuiescePinTests(7): SQL text of the job lookup, stop, start and worker count; the probe; the cap; a census that every product drop is preceded by the quiesce, that each catch reopens and resumes through one helper, and (new) that the stop is inside the try, no resume takes a caller token, and the cap decides on the final look.AggregateJobQuiesceLiveTests(8, own scratch store each), 15/15 with the pins, run 3 times in a row on a fresh rig (port 55510, UTC, CI's worker settings), all green with 0 skipped:pg_stat_activityasRefresh Continuous Aggregate Policy [id]. The test asserts the jobs are stopped while it runs; and on every look, while that same pid is alive on both sides of the look, the sweeper's OWN backend never shows the DROP inpg_stat_activity.query(matched asDROP MATERIALIZED VIEWor the start of theDO $do$block, becausetrack_activity_query_sizecuts the long text), the aggregate exists, and the sweep has not finished. It passes only if at least one look saw the worker alive and the sweeper inside its wait loop; if the scheduler ended the worker before that, the test SKIPS and says so. Then the drop goes ahead once the worker exits;XX000, which breaks the connection) schedules the jobs again after the reopen;pg_stat_activity), and, when the DROP finished too fast to catch, "the aggregate's jobs were dropped before its worker exited". Wait loop restored, 15/15 green after.RetiredBaselineAggregate*,StoreObjectConvergence*, and every non-live*Census*,*Ratchet*,*Hygiene*,*Guard*,*Pin*class (111 classes, 1311 tests, 0 failed, 2 skipped), plusLiveCleanupConversionRatchetTests,LivePostgresCollectionHygieneTestsandLivePlanShapeIsolationCensusTests. The*LiveTestsGuard/Pin classes were not run (no CPU load on the laptop; CI decides).Things to double-check
SELECT alter_job(<job id>, scheduled => true).RetiredBaselineSweepRecoveryLiveTestsrace test drives the job withCALL run_job, which runs in a client backend rather than a job worker, so the quiesce does not see it. That file belongs to Darling tests: a scratch database drop no longer hands its backend to the next test, and the retired-baseline race no longer runs a refresh beside the drop #5550 and is untouched; its assertions still pass.pg_stat_activity.backend_typefor the job owner's backends, which the service login (the owner of the aggregates) can read.Proposed changelog entry
Fixed: