From d75aedd8e85407a1cd5d1c103ecc938dfb64c1bd Mon Sep 17 00:00:00 2001 From: Rui Zhao Date: Sun, 13 Sep 2026 00:00:27 +0800 Subject: [PATCH 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 | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index de491ea0c4..76fcda55be 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -470,6 +470,55 @@ SnapBuildInitialSnapshot(SnapBuild *builder) snap = SnapBuildBuildSnapshot(builder); + /* + * The commit records of the transactions in snap->xip have been decoded, + * but the transactions themselves may not have finished committing: a + * transaction writes its commit record, then updates CLOG, then waits for + * synchronous replication if configured, and only then leaves the + * procarray. The snapshot built here is used by HeapTupleSatisfiesMVCC(), + * which takes these transactions as not running and consults CLOG about + * them, so every one of them has to have finished. Read the set of + * running transactions once and wait, on the transaction lock, for those + * of snap->xip that are still in it; the others have left the procarray + * and therefore have updated CLOG. + * + * A subtransaction is covered by its top-level transaction, which is in + * snap->xip as well, or was purged from it because it is below xmin and + * thus finished long ago. + * + * Historic snapshots do not need this: between xmin and xmax they rely on + * xip alone, and transactions below xmin had left the procarray by the + * time the xl_running_xacts record that set xmin was written. + * + * This is the same wait as in SnapBuildWaitSnapshot(). It is safe here + * because we are creating a slot or preparing REPACK, not streaming to a + * subscriber whose confirmation one of these transactions might be + * waiting for. + * + * During recovery the decoded commit record has been replayed already, + * and replaying it updates CLOG before the transaction stops being known + * as running, so there is nothing to wait for. + */ + if (!RecoveryInProgress()) + { + RunningTransactions running; + int nrunning; + + running = GetRunningTransactionData(); + nrunning = running->xcnt + running->subxcnt; + LWLockRelease(ProcArrayLock); + LWLockRelease(XidGenLock); + + for (int i = 0; i < nrunning; i++) + { + TransactionId running_xid = running->xids[i]; + + if (bsearch(&running_xid, snap->xip, snap->xcnt, + sizeof(TransactionId), xidComparator) != NULL) + XactLockTableWait(running_xid, NULL, NULL, XLTW_None); + } + } + /* * Building an initial snapshot is expensive and an unenforced xmin * horizon would have bad consequences, therefore always double-check that -- 2.43.7