In xact_redo_commit(), the standbyState != STANDBY_DISABLED case uses
TransactionIdAsyncCommitTree() on the grounds that we can't let hint
bits be set for transactions prior to minRecoveryPoint. Based on that
reasoning, the STANDBY_DISABLED case doesn't need to worry, because it
won't be setting hint bits.
But hint bits aren't the only problem: we also can't let the CLOG
status go to disk prior to minRecoveryPoint, because a PITR to just
before the transaction committed could still see it as committed. That
has nothing to do with whether hot standby is enabled or not, so the
STANDBY_DISABLED case is wrong, and it also must use the async
protocol.
The cleanest fix would be to lift TransactionIdAsyncCommitTree() out of
the branch, placing it prior to RecordKnownAssignedTransactionIds() on
the !STANDBY_DISABLED case. But the comment above
RecordKnownAssignedTransactionIds() is especially scary, so out of an
abundance of caution I just replaced the call site for the
STANDBY_DISABLED case. That seems wise for the back branches, and we
can get rid of the second call site in master in a separate commit.
Patch attached.
Regards,
Jeff Davis
From 9261cd1a31e7f9cb63cbffa36c330a8c6e114810 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Fri, 6 Nov 2026 13:41:57 -0800
Subject: [PATCH v1 1/2] Recovery: ensure CLOG updates wait for
minRecoveryPoint.
The hot_standby=on case already used TransactionIdAsyncCommitTree();
use that for hot_standby=off, too.
Before this commit, it was possible that the CLOG status could reach
disk without advancing minRecoveryPoint. Recovering with PITR prior to
the transaction's commit record could incorrectly see the transaction
as committed.
Backpatch-through: 14
---
src/backend/access/transam/xact.c | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 49e64939667..990440a964a 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -6243,9 +6243,14 @@ xact_redo_commit(xl_xact_parsed_commit *parsed,
if (standbyState == STANDBY_DISABLED)
{
/*
- * Mark the transaction committed in pg_xact.
+ * Mark the transaction committed in pg_xact. We use the async commit
+ * protocol during recovery to ensure that minRecoveryPoint is
+ * advanced before the CLOG update or any hint bit updates are written
+ * to disk. This ensures that if we crash, we don't see the committed
+ * status or hint bits set for transactions that haven't yet
+ * recovered.
*/
- TransactionIdCommitTree(xid, parsed->nsubxacts, parsed->subxacts);
+ TransactionIdAsyncCommitTree(xid, parsed->nsubxacts, parsed->subxacts, lsn);
}
else
{
@@ -6260,15 +6265,7 @@ xact_redo_commit(xl_xact_parsed_commit *parsed,
*/
RecordKnownAssignedTransactionIds(max_xid);
- /*
- * Mark the transaction committed in pg_xact. We use async commit
- * protocol during recovery to provide information on database
- * consistency for when users try to set hint bits. It is important
- * that we do not set hint bits until the minRecoveryPoint is past
- * this commit record. This ensures that if we crash we don't see hint
- * bits set on changes made by transactions that haven't yet
- * recovered. It's unlikely but it's good to be safe.
- */
+ /* use async, the same as the STANDBY_DISABLED case */
TransactionIdAsyncCommitTree(xid, parsed->nsubxacts, parsed->subxacts, lsn);
/*
--
2.53.0