From 91f4c1fda343562f555fc473f925966b97dc7b12 Mon Sep 17 00:00:00 2001
From: Rui Zhao <zhaorui126@gmail.com>
Date: Sun, 13 Sep 2026 00:00:27 +0800
Subject: [PATCH v6 1/3] Wait for the transactions of an initial decoding
 snapshot to finish

SnapBuildInitialSnapshot() converts the snapshot builder's list of
committed transactions into a regular MVCC snapshot, which is then used
with HeapTupleSatisfiesMVCC(). That function consults CLOG about the
transactions the snapshot takes as not running, so each of them has to
have finished committing before the snapshot is handed out: the commit
record is written first, CLOG is updated afterwards, and the transaction
stays in the procarray until after that.

Read the set of running transactions once, and wait on the transaction
lock of those that are in the snapshot's list, as SnapBuildWaitSnapshot()
does in the same code path; the others have left the procarray and so
have updated CLOG. Historic snapshots built by SnapBuildBuildSnapshot()
need no such wait: they rely on the xip array for transactions between
xmin and xmax, and consult CLOG only for transactions below xmin, which
had left the procarray when the xl_running_xacts record that set xmin was
written.
---
 src/backend/replication/logical/snapbuild.c | 33 +++++++++++++++++++++
 src/backend/storage/ipc/procarray.c         |  4 +--
 2 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index de491ea0c4b..5c51c65bce4 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -448,6 +448,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	TransactionId safeXid;
 	TransactionId *newxip;
 	int			newxcnt = 0;
+	RunningTransactions running = NULL;
 
 	Assert(XactIsoLevel == XACT_REPEATABLE_READ);
 	Assert(builder->building_full_snapshot);
@@ -493,6 +494,17 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	/* allocate in transaction context */
 	newxip = palloc_array(TransactionId, GetMaxSnapshotXidCount());
 
+	/*
+	 * Avoid excessive traffic through TransactionIdIsInProgress() below by
+	 * acquiring the list of running transactions once.
+	 */
+	if (!RecoveryInProgress())
+	{
+		running = GetRunningTransactionData();
+		LWLockRelease(XidGenLock);
+		LWLockRelease(ProcArrayLock);
+	}
+
 	/*
 	 * snapbuild.c builds transactions in an "inverted" manner, which means it
 	 * stores committed transactions in ->xip, not ones in progress. Build a
@@ -520,6 +532,27 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 			newxip[newxcnt++] = xid;
 		}
 
+		/*
+		 * If a transaction is in the snapshot and reported as committed, we
+		 * don't yet know for certain that the transaction was removed from
+		 * procarray as opposed to merely got its WAL commit record written.
+		 * For correctness reasons (involving hint-bit setting) we must not
+		 * allow transactions in the latter state be reported as committed, so
+		 * wait for them to end.
+		 */
+		if (!RecoveryInProgress() && test != NULL)
+		{
+			/* XXX We could qsort() and bsearch() this array ... */
+			for (int i = 0; i < running->xcnt; i++)
+			{
+				if (xid == running->xids[i])
+				{
+					XactLockTableWait(xid, NULL, NULL, XLTW_None);
+					break;
+				}
+			}
+		}
+
 		TransactionIdAdvance(xid);
 	}
 
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index b7e03134ed8..fd678bea3b6 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -2663,8 +2663,8 @@ GetRunningTransactionData(void)
 	 * the lock, so we can't look at numProcs.  Likewise, we allocate much
 	 * more subxip storage than is probably needed.
 	 *
-	 * Should only be allocated in bgwriter, since only ever executed during
-	 * checkpoints.
+	 * This is only called during checkpoint and during initial logical
+	 * decoding snapshot build, so the impact is limited.
 	 */
 	if (CurrentRunningXacts->xids == NULL)
 	{
-- 
2.47.3

