From fb51f8fb86cf80311681484be67987ede430dc83 Mon Sep 17 00:00:00 2001 From: Chee Wooson 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