On Sat, Aug 15, 2026 at 9:28 PM Imran Zaheer <[email protected]> wrote:
> I agree that tracking whether StartupSUBTRANS() has actually been
> called is more direct and robust. I don't have any strong objection to
> your approach; your fix looks reasonable to me.

Thanks for the review!

I've applied the cosmetic changes to the patch and created patches
for the older branches.


> My initial concern was more about having too many recovery state
> management variables at this point, i.e., ArchiveRecoveryRequested,
> InArchiveRecovery, EnableHotStandby, StandbyMode,
> StandbyModeRequested, etc. I just wanted to keep the context close to
> the existing states and did not want to create a new state for this
> specific bug.

I agree that adding such new recovery state variables basically would
not be a good idea. But, I don't think the flag introduced by this patch
falls into that category. It simply tracks whether pg_subtrans has been
started during recovery, so I don't have much concern about adding it.

Regards,

-- 
Fujii Masao
From 82029dca028c9427a821ef06e27e28198ce1aba6 Mon Sep 17 00:00:00 2001
From: Imran Zaheer <[email protected]>
Date: Sat, 15 Aug 2026 23:50:33 +0900
Subject: [PATCH v3] Fix checkpointer restartpoint assertion failure

When recovery starts from a backup without a signal file, pg_subtrans
is not started at the beginning of recovery and remains unstarted
throughout recovery. However, previously, a restartpoint run by
the checkpointer during recovery nevertheless tried to truncate
pg_subtrans, triggering the assertion failure:

    TRAP: failed Assert("TransactionIdIsValid(initial)")

This commit fixes this by tracking whether pg_subtrans has been
started during recovery, and have the checkpointer check this flag
before truncating pg_subtrans at restartpoints.

Backpatch to all supported versions.
---
 src/backend/access/transam/xlog.c         |  7 ++---
 src/backend/access/transam/xlogrecovery.c | 32 +++++++++++++++++++++++
 src/include/access/xlogrecovery.h         |  2 ++
 3 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/transam/xlog.c 
b/src/backend/access/transam/xlog.c
index f79961633a6..e9508fe9d7b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -5260,6 +5260,7 @@ StartupXLOG(void)
                         * during recovery and need not be started yet.
                         */
                        StartupSUBTRANS(oldestActiveXID);
+                       SetRecoverySubtransInitialized();
 
                        /*
                         * If we're beginning at a shutdown checkpoint, we know 
that
@@ -7243,10 +7244,10 @@ CreateRestartPoint(int flags)
         * Truncate pg_subtrans if possible.  We can throw away all data before
         * the oldest XMIN of any running transaction.  No future transaction 
will
         * attempt to reference any pg_subtrans entry older than that (see 
Asserts
-        * in subtrans.c).  When hot standby is disabled, though, we mustn't do
-        * this because StartupSUBTRANS hasn't been called yet.
+        * in subtrans.c).  During recovery, don't truncate pg_subtrans until 
hot
+        * standby initialization has started it.
         */
-       if (EnableHotStandby)
+       if (RecoverySubtransInitialized())
                TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());
 
        /* Real work is done; log and update stats. */
diff --git a/src/backend/access/transam/xlogrecovery.c 
b/src/backend/access/transam/xlogrecovery.c
index b07a54a9216..26738af34ba 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -313,6 +313,12 @@ typedef struct XLogRecoveryCtlData
         */
        bool            SharedPromoteIsTriggered;
 
+       /*
+        * SharedRecoverySubtransInitialized indicates whether hot standby
+        * initialization has started pg_subtrans. Protected by info_lck.
+        */
+       bool            SharedRecoverySubtransInitialized;
+
        /*
         * recoveryWakeupLatch is used to wake up the startup process to 
continue
         * WAL replay, if it is waiting for WAL to arrive or failover trigger 
file
@@ -4492,6 +4498,32 @@ CheckPromoteSignal(void)
        return false;
 }
 
+/*
+ * Has hot standby initialization started pg_subtrans?
+ */
+bool
+RecoverySubtransInitialized(void)
+{
+       bool            result;
+
+       SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+       result = XLogRecoveryCtl->SharedRecoverySubtransInitialized;
+       SpinLockRelease(&XLogRecoveryCtl->info_lck);
+
+       return result;
+}
+
+/*
+ * Remember that hot standby initialization has started pg_subtrans.
+ */
+void
+SetRecoverySubtransInitialized(void)
+{
+       SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+       XLogRecoveryCtl->SharedRecoverySubtransInitialized = true;
+       SpinLockRelease(&XLogRecoveryCtl->info_lck);
+}
+
 /*
  * Wake up startup process to replay newly arrived WAL, or to notice that
  * failover has been requested.
diff --git a/src/include/access/xlogrecovery.h 
b/src/include/access/xlogrecovery.h
index 0aa85d90e89..55ccbe23f0b 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -145,6 +145,8 @@ extern XLogRecPtr GetCurrentReplayRecPtr(TimeLineID 
*replayEndTLI);
 
 extern bool PromoteIsTriggered(void);
 extern bool CheckPromoteSignal(void);
+extern bool RecoverySubtransInitialized(void);
+extern void SetRecoverySubtransInitialized(void);
 extern void WakeupRecovery(void);
 
 extern void StartupRequestWalReceiverRestart(void);
-- 
2.55.0

Attachment: v3-0001-Fix-checkpointer-restartpoint-assertion-failure.patch
Description: Binary data

From f66e3160652b73637b157df5326bf67f6bc9e19a Mon Sep 17 00:00:00 2001
From: Imran Zaheer <[email protected]>
Date: Sun, 16 Aug 2026 00:08:56 +0900
Subject: [PATCH v3] Fix checkpointer restartpoint assertion failure

When recovery starts from a backup without a signal file, pg_subtrans
is not started at the beginning of recovery and remains unstarted
throughout recovery. However, previously, a restartpoint run by
the checkpointer during recovery nevertheless tried to truncate
pg_subtrans, triggering the assertion failure:

    TRAP: failed Assert("TransactionIdIsValid(initial)")

This commit fixes this by tracking whether pg_subtrans has been
started during recovery, and have the checkpointer check this flag
before truncating pg_subtrans at restartpoints.

Backpatch to all supported versions.
---
 src/backend/access/transam/xlog.c | 42 ++++++++++++++++++++++++++++---
 1 file changed, 39 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/transam/xlog.c 
b/src/backend/access/transam/xlog.c
index ec84c77ddfa..bb84525e665 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -689,6 +689,12 @@ typedef struct XLogCtlData
         */
        bool            SharedPromoteIsTriggered;
 
+       /*
+        * SharedRecoverySubtransInitialized indicates whether hot standby
+        * initialization has started pg_subtrans. Protected by info_lck.
+        */
+       bool            SharedRecoverySubtransInitialized;
+
        /*
         * WalWriterSleeping indicates whether the WAL writer is currently in
         * low-power mode (and hence should be nudged if an async commit 
occurs).
@@ -971,6 +977,8 @@ static void ReadControlFile(void);
 static char *str_time(pg_time_t tnow);
 static void SetPromoteIsTriggered(void);
 static bool CheckForStandbyTrigger(void);
+static bool RecoverySubtransInitialized(void);
+static void SetRecoverySubtransInitialized(void);
 
 #ifdef WAL_DEBUG
 static void xlog_outrec(StringInfo buf, XLogReaderState *record);
@@ -5295,6 +5303,7 @@ XLOGShmemInit(void)
        XLogCtl->SharedHotStandbyActive = false;
        XLogCtl->InstallXLogFileSegmentActive = false;
        XLogCtl->SharedPromoteIsTriggered = false;
+       XLogCtl->SharedRecoverySubtransInitialized = false;
        XLogCtl->WalWriterSleeping = false;
 
        SpinLockInit(&XLogCtl->Insert.insertpos_lck);
@@ -7308,6 +7317,7 @@ StartupXLOG(void)
                         * during recovery and need not be started yet.
                         */
                        StartupSUBTRANS(oldestActiveXID);
+                       SetRecoverySubtransInitialized();
 
                        /*
                         * If we're beginning at a shutdown checkpoint, we know 
that
@@ -10029,10 +10039,10 @@ CreateRestartPoint(int flags)
         * Truncate pg_subtrans if possible.  We can throw away all data before
         * the oldest XMIN of any running transaction.  No future transaction 
will
         * attempt to reference any pg_subtrans entry older than that (see 
Asserts
-        * in subtrans.c).  When hot standby is disabled, though, we mustn't do
-        * this because StartupSUBTRANS hasn't been called yet.
+        * in subtrans.c).  During recovery, don't truncate pg_subtrans until 
hot
+        * standby initialization has started it.
         */
-       if (EnableHotStandby)
+       if (RecoverySubtransInitialized())
                TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());
 
        /* Real work is done; log and update stats. */
@@ -13408,6 +13418,32 @@ CheckPromoteSignal(void)
        return false;
 }
 
+/*
+ * Has hot standby initialization started pg_subtrans?
+ */
+static bool
+RecoverySubtransInitialized(void)
+{
+       bool            result;
+
+       SpinLockAcquire(&XLogCtl->info_lck);
+       result = XLogCtl->SharedRecoverySubtransInitialized;
+       SpinLockRelease(&XLogCtl->info_lck);
+
+       return result;
+}
+
+/*
+ * Remember that hot standby initialization has started pg_subtrans.
+ */
+static void
+SetRecoverySubtransInitialized(void)
+{
+       SpinLockAcquire(&XLogCtl->info_lck);
+       XLogCtl->SharedRecoverySubtransInitialized = true;
+       SpinLockRelease(&XLogCtl->info_lck);
+}
+
 /*
  * Wake up startup process to replay newly arrived WAL, or to notice that
  * failover has been requested.
-- 
2.55.0

From 1ad2b525750aa519735b5c43522741809076904f Mon Sep 17 00:00:00 2001
From: Imran Zaheer <[email protected]>
Date: Sat, 15 Aug 2026 23:35:35 +0900
Subject: [PATCH v3] Fix checkpointer restartpoint assertion failure

When recovery starts from a backup without a signal file, pg_subtrans
is not started at the beginning of recovery and remains unstarted
throughout recovery. However, previously, a restartpoint run by
the checkpointer during recovery nevertheless tried to truncate
pg_subtrans, triggering the assertion failure:

    TRAP: failed Assert("TransactionIdIsValid(initial)")

This commit fixes this by tracking whether pg_subtrans has been
started during recovery, and have the checkpointer check this flag
before truncating pg_subtrans at restartpoints.

Backpatch to all supported versions.
---
 src/backend/access/transam/xlog.c         |  7 ++---
 src/backend/access/transam/xlogrecovery.c | 32 +++++++++++++++++++++++
 src/include/access/xlogrecovery.h         |  2 ++
 3 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/transam/xlog.c 
b/src/backend/access/transam/xlog.c
index 2fd06e37999..4165f31f921 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -5844,6 +5844,7 @@ StartupXLOG(void)
                         * during recovery and need not be started yet.
                         */
                        StartupSUBTRANS(oldestActiveXID);
+                       SetRecoverySubtransInitialized();
 
                        /*
                         * If we're beginning at a shutdown checkpoint, we know 
that
@@ -7856,10 +7857,10 @@ CreateRestartPoint(int flags)
         * Truncate pg_subtrans if possible.  We can throw away all data before
         * the oldest XMIN of any running transaction.  No future transaction 
will
         * attempt to reference any pg_subtrans entry older than that (see 
Asserts
-        * in subtrans.c).  When hot standby is disabled, though, we mustn't do
-        * this because StartupSUBTRANS hasn't been called yet.
+        * in subtrans.c).  During recovery, don't truncate pg_subtrans until 
hot
+        * standby initialization has started it.
         */
-       if (EnableHotStandby)
+       if (RecoverySubtransInitialized())
                TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());
 
        /* Real work is done; log and update stats. */
diff --git a/src/backend/access/transam/xlogrecovery.c 
b/src/backend/access/transam/xlogrecovery.c
index 37d6090e465..d94b291495d 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -322,6 +322,12 @@ typedef struct XLogRecoveryCtlData
         */
        bool            SharedPromoteIsTriggered;
 
+       /*
+        * SharedRecoverySubtransInitialized indicates whether hot standby
+        * initialization has started pg_subtrans. Protected by info_lck.
+        */
+       bool            SharedRecoverySubtransInitialized;
+
        /*
         * recoveryWakeupLatch is used to wake up the startup process to 
continue
         * WAL replay, if it is waiting for WAL to arrive or promotion to be
@@ -4523,6 +4529,32 @@ CheckPromoteSignal(void)
        return false;
 }
 
+/*
+ * Has hot standby initialization started pg_subtrans?
+ */
+bool
+RecoverySubtransInitialized(void)
+{
+       bool            result;
+
+       SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+       result = XLogRecoveryCtl->SharedRecoverySubtransInitialized;
+       SpinLockRelease(&XLogRecoveryCtl->info_lck);
+
+       return result;
+}
+
+/*
+ * Remember that hot standby initialization has started pg_subtrans.
+ */
+void
+SetRecoverySubtransInitialized(void)
+{
+       SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+       XLogRecoveryCtl->SharedRecoverySubtransInitialized = true;
+       SpinLockRelease(&XLogRecoveryCtl->info_lck);
+}
+
 /*
  * Wake up startup process to replay newly arrived WAL, or to notice that
  * failover has been requested.
diff --git a/src/include/access/xlogrecovery.h 
b/src/include/access/xlogrecovery.h
index 91446303024..771ff1ea23a 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -146,6 +146,8 @@ extern XLogRecPtr GetCurrentReplayRecPtr(TimeLineID 
*replayEndTLI);
 
 extern bool PromoteIsTriggered(void);
 extern bool CheckPromoteSignal(void);
+extern bool RecoverySubtransInitialized(void);
+extern void SetRecoverySubtransInitialized(void);
 extern void WakeupRecovery(void);
 
 extern void StartupRequestWalReceiverRestart(void);
-- 
2.55.0

Reply via email to