From f98f7002d15712392d4a4a861f66e3e728023cc5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Wed, 7 Oct 2026 12:42:01 +0200
Subject: [PATCH v6 2/3] Kuroda

---
 src/backend/replication/logical/decode.c      | 14 +--
 .../replication/logical/reorderbuffer.c       | 89 ++++++++++++++-----
 src/include/replication/reorderbuffer.h       |  6 +-
 3 files changed, 77 insertions(+), 32 deletions(-)

diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 0e7d4840a8e..c45edae853a 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -895,9 +895,9 @@ DecodeAbort(LogicalDecodingContext *ctx, XLogRecordBuffer *buf,
 		/*
 		 * Remove tuplecid changes queued by the aborted (sub)transactions
 		 * from the toplevel's list, before the transactions are torn down.
-		 * The abort record's primary xid tells
-		 * ReorderBufferCleanupSubTxnTupleCids() whether the whole toplevel
-		 * is going away, in which case scanning the list is pointless.
+		 * The abort record's primary xid tells the cleanup function whether
+		 * the whole toplevel is going away, in which case scanning the list is
+		 * pointless.
 		 *
 		 * Note that we must not try to decide that from the primary xid's
 		 * own association instead: an abort record is written once the
@@ -909,16 +909,16 @@ DecodeAbort(LogicalDecodingContext *ctx, XLogRecordBuffer *buf,
 		 * all, while the released inner subtransactions it rolls back do
 		 * have one and their tuplecids still need to be removed.
 		 */
+		ReorderBufferCleanupAbortedSubTxnTupleCids(ctx->reorder, xid,
+												   parsed->nsubxacts,
+												   parsed->subxacts);
+
 		for (i = 0; i < parsed->nsubxacts; i++)
 		{
-			ReorderBufferCleanupSubTxnTupleCids(ctx->reorder,
-												parsed->subxacts[i],
-												xid);
 			ReorderBufferAbort(ctx->reorder, parsed->subxacts[i],
 							   buf->record->EndRecPtr, abort_time);
 		}
 
-		ReorderBufferCleanupSubTxnTupleCids(ctx->reorder, xid, xid);
 		ReorderBufferAbort(ctx->reorder, xid, buf->record->EndRecPtr,
 						   abort_time);
 	}
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index e1c6a7c4d32..ccbdbde7335 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -240,6 +240,9 @@ static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
 											   XLogRecPtr lsn, bool create_as_top);
 static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
 											  ReorderBufferTXN *subtxn);
+static bool TransactionIdInSubxactArray(TransactionId xid,
+										TransactionId *subxacts,
+										int nsubxacts);
 
 static void AssertTXNLsnOrder(ReorderBuffer *rb);
 
@@ -3158,8 +3161,8 @@ ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn,
 }
 
 /*
- * Remove tuplecid changes queued by subtransaction xid from its toplevel
- * transaction's list.
+ * Remove tuplecid changes queued by aborted subtransactions from their
+ * toplevel transaction's list.
  *
  * Unlike regular changes, tuplecid changes are always queued on the toplevel
  * transaction (see ReorderBufferAddNewTupleCids), so they would otherwise
@@ -3174,15 +3177,10 @@ ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn,
  *
  * The cleanup is pointless when the whole toplevel transaction is
  * being aborted, since ReorderBufferCleanupTXN() frees the whole list
- * anyway; the caller passes the abort record's primary xid and we skip
- * the scan when it is xid's toplevel.  Note that the opposite decision
- * -- cleaning only when the primary xid's own association is known --
- * would be wrong: abort records are written once the subtransaction is
- * already in TRANS_ABORT, so they carry no toplevel xid in their
- * header, and an outer subtransaction that never wrote WAL of its own
- * never gets an association at all.  When such an outer subtransaction
- * rolls back, released inner subtransactions listed in its abort record
- * do have known associations and are cleaned here.
+ * anyway.  If the primary xid is unknown, search its aborted children for a known
+ * association.  An outer subtransaction that never wrote WAL of its own can
+ * have no association, while released inner subtransactions listed in its
+ * abort record do have one and their tuplecids still need to be removed.
  *
  * If this pass does not know the association between the aborting
  * subtransaction and its toplevel, there is nothing we can clean up here, and
@@ -3198,8 +3196,10 @@ ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn,
  * transaction state without ever reaching ReorderBufferBuildTupleCidHash().
  */
 void
-ReorderBufferCleanupSubTxnTupleCids(ReorderBuffer *rb, TransactionId xid,
-									TransactionId primary_xid)
+ReorderBufferCleanupAbortedSubTxnTupleCids(ReorderBuffer *rb,
+										   TransactionId xid,
+										   int nsubxacts,
+										   TransactionId *subxacts)
 {
 	ReorderBufferTXN *txn;
 	ReorderBufferTXN *toptxn;
@@ -3208,19 +3208,39 @@ ReorderBufferCleanupSubTxnTupleCids(ReorderBuffer *rb, TransactionId xid,
 	txn = ReorderBufferTXNByXid(rb, xid, false, NULL, InvalidXLogRecPtr,
 								false);
 
-	/* unknown transaction, or unknown association: nothing to remove */
-	if (txn == NULL || !rbtxn_is_known_subxact(txn))
+	/*
+	 * If xid is a known toplevel transaction, the whole transaction is being
+	 * aborted and ReorderBufferCleanupTXN() will free the tuplecid list.
+	 */
+	if (txn != NULL && !rbtxn_is_known_subxact(txn))
 		return;
 
-	toptxn = rbtxn_get_toptxn(txn);
-
 	/*
-	 * The whole toplevel transaction is being aborted (the abort record's
-	 * primary xid is xid's toplevel): ReorderBufferCleanupTXN() will free
-	 * the whole list shortly, don't scan it once per aborted subxid.
+	 * If the aborting subtransaction is unknown, try to find the toplevel
+	 * transaction through one of its children. There may be any number of
+	 * unknown children, but they cannot have queued tuplecid changes: decoding
+	 * a WAL record that queues such a change first associates its xid with the
+	 * toplevel transaction.  Therefore, if all children are unknown, there is
+	 * nothing to remove.
 	 */
-	if (toptxn->xid == primary_xid)
-		return;
+	if (txn == NULL)
+	{
+		for (int i = 0; i < nsubxacts; i++)
+		{
+			txn = ReorderBufferTXNByXid(rb, subxacts[i], false, NULL,
+										InvalidXLogRecPtr, false);
+			if (txn != NULL && rbtxn_is_known_subxact(txn))
+				break;
+
+			txn = NULL;
+		}
+
+		if (txn == NULL)
+			return;
+	}
+
+	/* Get the top-level transaction to clean up its tuplecid list */
+	toptxn = rbtxn_get_toptxn(txn);
 
 	dlist_foreach_modify(it, &toptxn->tuplecids)
 	{
@@ -3230,15 +3250,38 @@ ReorderBufferCleanupSubTxnTupleCids(ReorderBuffer *rb, TransactionId xid,
 
 		Assert(change->action == REORDER_BUFFER_CHANGE_INTERNAL_TUPLECID);
 
-		if (change->data.tuplecid.subxid == xid)
+		/*
+		 * Remove the entry if it's part of the aborting subtransaction or one
+		 * of its children.
+		 */
+		if (change->data.tuplecid.subxid == xid ||
+			TransactionIdInSubxactArray(change->data.tuplecid.subxid,
+										subxacts, nsubxacts))
 		{
 			dlist_delete(&change->node);
 			ReorderBufferFreeChange(rb, change, false);
+			Assert(toptxn->ntuplecids > 0);
 			toptxn->ntuplecids--;
 		}
 	}
 }
 
+/*
+ * Check whether xid is in an array sorted in logical XID order.
+ */
+static bool
+TransactionIdInSubxactArray(TransactionId xid, TransactionId *subxacts,
+							int nsubxacts)
+{
+	/*
+	 * An abort record's subxacts are children that previously subcommitted
+	 * into the aborting transaction.  AtSubCommit_childXids() preserves their
+	 * logical XID order, so xidLogicalComparator can safely compare them.
+	 */
+	return bsearch(&xid, subxacts, nsubxacts,
+				   sizeof(TransactionId), xidLogicalComparator) != NULL;
+}
+
 /*
  * Abort all transactions that aren't actually running anymore because the
  * server restarted.
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 23c447c4953..7cfa43ad163 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -743,8 +743,10 @@ extern void ReorderBufferCommitChild(ReorderBuffer *rb, TransactionId xid,
 									 XLogRecPtr end_lsn);
 extern void ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn,
 							   TimestampTz abort_time);
-extern void ReorderBufferCleanupSubTxnTupleCids(ReorderBuffer *rb, TransactionId xid,
-												TransactionId primary_xid);
+extern void ReorderBufferCleanupAbortedSubTxnTupleCids(ReorderBuffer *rb,
+													   TransactionId xid,
+													   int nsubxacts,
+													   TransactionId *subxacts);
 extern void ReorderBufferAbortOld(ReorderBuffer *rb, TransactionId oldestRunningXid);
 extern void ReorderBufferForget(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn);
 extern void ReorderBufferInvalidate(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn);
-- 
2.47.3

