Repository navigation
A store upgrade that fails before its commit point puts the old cluster back as it found it, and only claims a revert that happened (#3927) - #3954
Merged
Conversation
…er back as it found it, and only claims a revert that happened (#3927) Three defects in UpgradeDataDirectoryAsync's pre-commit failure paths, all reproduced on copies of a real 17.10 + TimescaleDB 2.28.1 store. In hard-link mode pg_upgrade renames the old cluster's global\pg_control once linking starts, and nothing renamed it back, so a failure after that point left a store that could not start on either runtime while the log said the data directory was never modified. RevertRuntime moved the runtimes under a postmaster the upgrade could not stop, which Windows allows, leaving a server whose binaries had moved. And a revert whose second move failed left no pgsql directory at all. The outcome and the Failed alert claimed a clean revert through all three. - The pre-commit catch runs RecoverFromPreCommitFailureAsync, and the cancel path does the same work in place. Both put the pre-upgrade data directory back BEFORE the runtime revert. In hard-link mode RestoreLinkedControlFile renames pg_control.old back, only when pg_control.old exists and pg_control does not. A failed rename is CRITICAL, names the exact manual rename, and the store is never called running. - The put-back also retries the swap's own undo: if the swap moved the store to its retained name and could not move it back, nothing is at the configured path, the next start would initdb an EMPTY store there, and the retention sweep would delete the real one two starts later. - RevertRuntime returns whether it reverted. It refuses while postmaster.pid names a live postgres process (a reused PID naming anything else is not a server), undoes a half-finished move, and no longer reports a completed revert as failed when only the blocked marker will not write. - The outcome carries PreUpgradeData and RuntimeReverted (trailing, defaulted to the clean revert), the worker's Failed arm maps them, and the Failed alert states only what happened for each shape. - TryStopAsync no longer says "the next start adopts it". Tests: DarlingStoreUpgradeRevertTests (30), with no PostgreSQL. The restore helper (restore, never overwrites, a rename that fails); the recovery seam in link mode with a planted pg_control.old and an injected failure, in copy mode, under a live fake postmaster (ping copied as postgres.exe), and with the store stranded under its retained name; the real pre-commit and cancel catch clauses; RevertRuntime's refusal, stale PIDs, the half-move undo and the marker; the worker mapping; and the alert text for each shape. Each fix was red-watched by reverting it: 13 mutations, each failing only the tests aimed at it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFctxdyLSdZbycGtvwXk8j
…upgrade-revert Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFctxdyLSdZbycGtvwXk8j
This was referenced Sep 23, 2026
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.
Closes #3927.
CHANGELOG: this PR carries no CHANGELOG edit. Its entry lands with the batched splice.
Why
DarlingStoreUpgrade.UpgradeDataDirectoryAsynchad three defects in its failure paths before the commit point. An adversarial review reproduced all three on copies of a real 17.10 + TimescaleDB 2.28.1 store.global/pg_controltopg_control.old, so the old cluster cannot be started while it shares files with the new one. The pre-commit handler never renamed it back, so any later failure left a store that could not start on either runtime. That includes pg_upgrade failing late and the directory swap failing. The handler then logged "NO data has been lost (the pre-upgrade data directory was never modified)". Review measurement E7a: renaming it back gives an intact old cluster when the new one was never started outside pg_upgrade, which is also what PostgreSQL's pg_upgrade docs say.RevertRuntimemoved the runtimes under a live postmaster.TryStopAsynconly warned, and on Windows bothDirectory.Movecalls succeed under a running server (E12/E13). The next connection was served by an 18.4 backend under an 18.6 postmaster. The next start adopted that postmaster, becausepg_ctl statusanswers "running" for one on any port, and then could not connect.pgsqlmoved topgsql.failedand the second move failed, there was nopgsqldirectory at all.The Failed outcome and the Failed self-alert claimed a clean revert ("reverted, collecting normally, no data was lost") in all three cases.
What changes
RecoverFromPreCommitFailureAsync(its own method so a test can inject exactly this failure). That method stops the old cluster if this upgrade started it, drops the half-built cluster, puts the pre-upgrade data directory back, reverts the runtime, and only then says what happened.!swapped). It does the same put-back in place, inside the guard and beforeRevertRuntimeForCancel, which keeps the existing containment pin's shape.RestoreLinkedControlFileis a static helper that renamespg_control.oldback. It runs in hard-link mode only, and only whenpg_control.oldexists andpg_controldoes not, so it can never overwrite a control file. It logs what it did. A failed rename is CRITICAL, names the exact manual rename, and nothing after it calls the store running.RevertRuntimereturns whether it reverted (existing call sites still compile and discard the value):postmaster.pidnames a live postgres process. A reused PID naming anything else is not a server.pgsql.failedback and reports no revert.StoreUpgradeOutcomegainsPreUpgradeData(Untouched,ControlFileRestored,NotRestored) andRuntimeReverted, both trailing and defaulted to the clean revert.UsedLinkModeis now true on a link-mode failure. The worker's Failed arm maps these to three new trailing report fields.TryStopAsyncno longer ends "the next start adopts it", and the class and method doc comments no longer promise a store that is never left down.Also fixed, same handler: the swap's own undo can fail
Step 7 moves the store to its retained name and moves it back when the second move fails. If that move back fails too, nothing is at the configured path. The old handler then called the store back. The next start ran initdb for an EMPTY store there, and the retention sweep deleted the real one (an unmarked
pg-old-N) two starts later. The put-back now retries that move once. If the retry fails, it logs CRITICAL naming the move, thepg_control.oldrename inside it in hard-link mode, and why restarting first would be the expensive mistake. Step 7 itself is untouched.Filed, not fixed here: #3951
CompleteAfterStartAsyncrewrites a Failed outcome toExtensionUpdatedwhenever the post-start extension update runs, andBuildStoreUpgradeReportmaps that to no alert. After #3908 bumps TimescaleDB, that would suppress this PR's "runtime NOT reverted" alert.CompleteAfterStartAsyncis #3908's to rewrite. #3951 also tracks the now-staleStoreUpgradeStatus.Faileddoc line, which is next to #3908's edit of that enum.Coordination with #3908
The #3908 branch edits the same file. This PR stays inside:
TryStopAsync,RevertRuntimeandRevertRuntimeForCancel;RevertRuntimeForCancel;StoreUpgradeOutcomeandStoreUpgradeReport;None of these overlaps a #3908 hunk.
BridgeTimescaleAsync, steps 1-7,StartClusterAsync/StopClusterAsync,CompleteAfterStartAsync,TryAdvanceRuntimeAsync,StoreUpgradeStatusandDarlingManagedPostgres.csare unchanged. The new tests referenceRuntimeStampFileNamesymbolically, so #3908's stamp rename flows through.Test plan
The new class is
DarlingStoreUpgradeRevertTests(30 tests). None of them needs PostgreSQL:Runtimes and data directories are marker files.
A live postmaster is
PING.EXEcopied aspostgres.exe, placed where the real one runs from. A running image does not block a directory move, and I measured that too.Every failure is a file held open, which blocks a rename.
Restore helper: restores; never overwrites (both files, neither file); a rename it cannot make is CRITICAL and names the rename.
Recovery seam, link mode: a planted
pg_control.oldand a failure injected before the commit point. The control file comes back before the runtime revert (log order), and the outcome isControlFileRestored, reverted,UsedLinkMode.Recovery seam, other shapes: a restore that fails; copy mode; a live server the upgrade could not stop; the store stranded under its retained name, moved back or not movable.
The real catch clauses: the real
UpgradeDataDirectoryAsyncfailing atstart-old-cluster, and the same run with a pre-cancelled token.RevertRuntime: refusal under a live postgres, stale PIDs (another process, no process, garbage, blank), a negated single-user PID, the half-move undo, the marker, the plain revert.The seam to the alert: the worker's Failed-arm mapping, and the alert text for each new shape.
DarlingStoreUpgradeTests79 (4 gated skips),DarlingSelfAlertTests215 (3 gated skips),AlertStoredValueTests16.Whole non-live
Darling.Testson the merged tree: 12,554, 0 failed (530 gated skips).Darling.TestsRelease build: 0 warnings, 0 errors.Red-watch
Each mutation reverted one fix, then the test project was rebuilt and the class re-run. Every mutation failed only the tests aimed at it.
pg_controlRevertRuntimerefusal, and recovery under a live serverpg_controlNot verified here: no live pg_upgrade was run. The hard limits kept ports 55432/55433 and live upgrades off-limits, so the E7a/E12/E13 measurements are the review's, on real stores. These tests pin the behavior around them.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SFctxdyLSdZbycGtvwXk8j