From fb51f8fb86cf80311681484be67987ede430dc83 Mon Sep 17 00:00:00 2001
From: Chee Wooson <wuqi@vastdata.com.cn>
Date: Mon, 28 Sep 2026 17:31:56 +0800
Subject: [PATCH v3 2/3] Judge a finished multixact updater by commit status in
 heap_update

A crashed updater has neither an abort nor a commit record.  With a
surviving prepared locker, heap_update can reach its post-conflict
verdict and report the crashed updater as a concurrent update.  That
leads to a spurious TM_Updated result.

At this point a conflicting updater has either been waited for or was
proven not running by the conflict check.  Use DidCommit to distinguish
an actual update from an aborted or crashed updater.
---
 src/backend/access/heap/heapam.c | 21 +++++++++++++--------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 17503e2fd1b..1f00bfdf938 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -3606,10 +3606,12 @@ l2:
 			 * Xmax.
 			 *
 			 * Note that there could have been another update in the
-			 * MultiXact. In that case, we need to check whether it committed
-			 * or aborted. If it aborted we are safe to update it again;
-			 * otherwise there is an update conflict, and we have to return
-			 * TableTuple{Deleted, Updated} below.
+			 * MultiXact. In that case, we need to check whether it
+			 * committed: only a committed updater means an update conflict,
+			 * and we have to return TableTuple{Deleted, Updated} below.
+			 * One that did not commit can no longer commit (reaching here
+			 * means it is no longer running), so we are safe to update it
+			 * again.
 			 *
 			 * In the LockTupleExclusive case, we still need to preserve the
 			 * surviving members: those would include the tuple locks we had
@@ -3622,12 +3624,15 @@ l2:
 				update_xact = InvalidTransactionId;
 
 			/*
-			 * There was no UPDATE in the MultiXact; or it aborted. No
-			 * TransactionIdIsInProgress() call needed here, since we called
-			 * MultiXactIdWait() above.
+			 * There was no UPDATE in the MultiXact; or it can no longer
+			 * have committed (aborted or crashed).  Reaching here means the
+			 * updater is no longer running: the wait above, or the conflict
+			 * check having skipped it, already proved that.  A not-running
+			 * xid can never become committed, so testing DidCommit is
+			 * enough -- no TransactionIdIsInProgress() call needed here.
 			 */
 			if (!TransactionIdIsValid(update_xact) ||
-				TransactionIdDidAbort(update_xact))
+				!TransactionIdDidCommit(update_xact))
 				can_continue = true;
 		}
 		else if (TransactionIdIsCurrentTransactionId(xwait))
-- 
2.43.0
