From a3bdc5adb7d374c9b34b2852543ab67c4dfb17f5 Mon Sep 17 00:00:00 2001
From: Chee Wooson <wuqi@vastdata.com.cn>
Date: Tue, 15 Sep 2026 14:21:51 +0800
Subject: [PATCH v2 1/2] Discard aborted updaters in MultiXactIdExpand()

An updater can be marked aborted in pg_xact while still in ProcArray.
DoesMultiXactIdConflict() allows another update in this window, but
MultiXactIdExpand() retains the old updater because it appears running.
Adding the new updater then fails with "new multixact has more than one
updating member".

Discard explicitly aborted updaters, matching DoesMultiXactIdConflict().
Keep the existing liveness checks to also discard crashed updaters that
have no abort record in pg_xact.
---
 src/backend/access/transam/multixact.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 70a4ea69486..340f2631dd1 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -486,6 +486,16 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
 
 	for (i = 0, j = 0; i < nmembers; i++)
 	{
+		/*
+		 * An updater can be marked aborted in pg_xact before leaving
+		 * ProcArray. Discard it even if it appears running, since a new
+		 * update need not wait for it and must not create a multixact with
+		 * two updating members.
+		 */
+		if (ISUPDATE_from_mxstatus(members[i].status) &&
+			TransactionIdDidAbort(members[i].xid))
+			continue;
+
 		if (TransactionIdIsInProgress(members[i].xid) ||
 			(ISUPDATE_from_mxstatus(members[i].status) &&
 			 TransactionIdDidCommit(members[i].xid)))
-- 
2.43.0

