From a51242fbe89c599ed2ae38cd02dd138a53a874c5 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Wed, 28 Oct 2026 23:14:06 -0400 Subject: [PATCH v1 1/2] Delay checkpoint start in RecordTransactionAbortPrepared() ROLLBACK PREPARED writes its abort record, flushes it, and only then marks the transaction aborted in pg_xact. Nothing kept a concurrent checkpoint from setting its redo pointer between those two steps. Such a checkpoint still writes out the two-phase state file (or finds it already on disk), and its CLOG flush does not include the abort. If the server crashes before the state file removal is durable, recovery starts after the abort record, finds the state file, sees nothing in pg_xact, and restores the transaction as prepared. It can then be committed with COMMIT PREPARED, although the rollback was already acknowledged. For a regular transaction, losing the abort is harmless because a transaction that is not marked committed is presumed aborted. For a prepared transaction that is not true, since the state file brings it back. Fix by setting DELAY_CHKPT_START around the abort record and the pg_xact update, as RecordTransactionCommitPrepared() already does for commits. Backpatch-through: 14 --- src/backend/access/transam/twophase.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index 48a83501ee9..77c8dae0fdc 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -2494,6 +2494,16 @@ RecordTransactionAbortPrepared(TransactionId xid, START_CRIT_SECTION(); + /* + * Keep a concurrent checkpoint from setting its redo pointer after our + * abort record until pg_xact is updated. Unlike a regular abort, losing + * the abort of a prepared transaction is not harmless: the checkpoint may + * write out our two-phase state file, and without the abort in pg_xact + * the transaction would come back as prepared after a crash. + */ + Assert((MyProc->delayChkptFlags & DELAY_CHKPT_START) == 0); + MyProc->delayChkptFlags |= DELAY_CHKPT_START; + /* * Emit the XLOG commit record. Note that we mark 2PC aborts as * potentially having AccessExclusiveLocks since we don't know whether or @@ -2515,11 +2525,14 @@ RecordTransactionAbortPrepared(TransactionId xid, XLogFlush(recptr); /* - * Mark the transaction aborted in clog. This is not absolutely necessary - * but we may as well do it while we are here. + * Mark the transaction aborted in clog. Recovery relies on this to + * discard a state file that a checkpoint wrote before we removed it. */ TransactionIdAbortTree(xid, nchildren, children); + /* Checkpoint can proceed now */ + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); /* -- 2.37.1 (Apple Git-137.1)