From 94052205c513586ca7a0b11d3ab4b891b8c562e8 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Thu, 29 Oct 2026 21:51:30 -0400 Subject: [PATCH v1 1/4] Advance oldestXid before truncating CLOG vac_truncate_clog() wrote the CLOG truncation record and removed the old pg_xact segments, and only then called SetTransactionIdLimit(). A checkpoint whose redo pointer lands between the two steps still records the old oldestXid. Crash recovery from that checkpoint does not replay the truncation record, and StartupXLOG() sets oldestClogXid from the checkpoint's oldestXid. oldestClogXid then points into removed segments, and pg_xact_status() on an XID in that range fails with "could not access status of transaction" instead of returning NULL. The next VACUUM does not fix it, since there is no segment left to remove. The same happens when starting from a base backup or a restartpoint taken in that window. Fix by calling SetTransactionIdLimit() before TruncateCLOG(), as we already do for commit timestamps with AdvanceOldestCommitTsXid(). A checkpoint whose redo pointer is after the truncation record then sees the new oldestXid. If we crash after advancing oldestXid but before truncating, the old segments are left behind until the next truncation, which is what already happens if we crash before the truncation record. Backpatch-through: 14 --- src/backend/access/transam/xlog.c | 4 +++- src/backend/commands/vacuum.c | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 8636ad9ab2c..29237579e9e 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -7941,7 +7941,9 @@ CreateCheckPoint(int flags) * We don't need to save oldestClogXid in the checkpoint, it only matters * for the short period in which clog is being truncated, and if we crash * during that we'll redo the clog truncation and fix up oldestClogXid - * there. + * there. If our redo pointer is after the clog truncation record, we see + * the new oldestXid, because vac_truncate_clog() advances it before + * truncating. */ LWLockAcquire(XidGenLock, LW_SHARED); checkPoint.nextXid = TransamVariables->nextXid; diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index c7595d59bb2..ccd1c56eaa8 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -1992,6 +1992,16 @@ vac_truncate_clog(TransactionId frozenXID, */ AdvanceOldestCommitTsXid(frozenXID); + /* + * Update the wrap limit for GetNewTransactionId. Do this before + * truncating CLOG, so that a checkpoint starting after the truncation + * record sees the new oldestXid. Crash recovery from such a checkpoint + * does not replay the truncation, and sets oldestClogXid from the + * checkpoint's oldestXid. Note: this function will also signal the + * postmaster for an autovac cycle if needed. + */ + SetTransactionIdLimit(frozenXID, oldestxid_datoid); + /* * Truncate CLOG, multixact and CommitTs to the oldest computed value. */ @@ -2000,12 +2010,10 @@ vac_truncate_clog(TransactionId frozenXID, TruncateMultiXact(minMulti, minmulti_datoid); /* - * Update the wrap limit for GetNewTransactionId and creation of new - * MultiXactIds. Note: these functions will also signal the postmaster - * for an(other) autovac cycle if needed. XXX should we avoid possibly - * signaling twice? + * Update the wrap limit for creation of new MultiXactIds. Note: this + * function will also signal the postmaster for an(other) autovac cycle if + * needed. XXX should we avoid possibly signaling twice? */ - SetTransactionIdLimit(frozenXID, oldestxid_datoid); SetMultiXactIdLimit(minMulti, minmulti_datoid); LWLockRelease(WrapLimitsVacuumLock); -- 2.37.1 (Apple Git-137.1)