From 78d8329fc2c1f10a96710dd3a3bb2f85fe324672 Mon Sep 17 00:00:00 2001
From: Chee Wooson <wuqi@vastdata.com.cn>
Date: Tue, 15 Sep 2026 14:21:54 +0800
Subject: [PATCH v2 2/2] Test multixact expansion during updater abort

Add an injection point after recording an abort in pg_xact and before
ProcArray cleanup.  Pause an updater there after a key-share lock has
made the tuple's xmax a multixact, then verify that another update
succeeds without retaining the aborted updater.
---
 src/backend/access/transam/xact.c             |  7 +++
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/multixact-aborted-updater.out    | 28 +++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/multixact-aborted-updater.spec      | 59 +++++++++++++++++++
 5 files changed, 97 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/multixact-aborted-updater.out
 create mode 100644 src/test/modules/injection_points/specs/multixact-aborted-updater.spec

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index aca92507ebd..9fbf1212493 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -65,6 +65,7 @@
 #include "utils/builtins.h"
 #include "utils/combocid.h"
 #include "utils/guc.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/relmapper.h"
@@ -1915,6 +1916,12 @@ RecordTransactionAbort(bool isSubXact)
 	if (ndroppedstats)
 		pfree(droppedstats);
 
+	/*
+	 * Test the window where the transaction is aborted in pg_xact but still
+	 * present in ProcArray.
+	 */
+	INJECTION_POINT("transaction-abort-after-clog", NULL);
+
 	return latestXid;
 }
 
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 9d8b4b3540c..e98aa240ced 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -27,7 +27,8 @@ ISOLATION = basic \
 	    syscache-update-pruned \
 	    wait_cleanup \
 	    heap_lock_update \
-	    on_conflict_probe_window
+	    on_conflict_probe_window \
+	    multixact-aborted-updater
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/multixact-aborted-updater.out b/src/test/modules/injection_points/expected/multixact-aborted-updater.out
new file mode 100644
index 00000000000..de5f5bb94dc
--- /dev/null
+++ b/src/test/modules/injection_points/expected/multixact-aborted-updater.out
@@ -0,0 +1,28 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s1begin s1update s2lock s1abort s3update wake
+step s1begin: BEGIN;
+step s1update: UPDATE mxact_abort SET filler = 's1' WHERE id = 1;
+step s2lock: SELECT * FROM mxact_abort WHERE id = 1 FOR KEY SHARE;
+id|filler 
+--+-------
+ 1|initial
+(1 row)
+
+step s1abort: ROLLBACK; <waiting ...>
+s3: NOTICE:  session 3 update succeeded
+step s3update: 
+	DO $$
+	BEGIN
+		UPDATE mxact_abort SET filler = 's3' WHERE id = 1;
+		RAISE NOTICE 'session 3 update succeeded';
+	EXCEPTION WHEN OTHERS THEN
+		RAISE NOTICE 'session 3 update failed: %', split_part(SQLERRM, ':', 1);
+	END
+	$$;
+
+step wake: 
+	SELECT FROM injection_points_detach('transaction-abort-after-clog');
+	SELECT FROM injection_points_wakeup('transaction-abort-after-clog');
+
+step s1abort: <... completed>
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 80a09f34d78..e99ebb356c2 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -57,6 +57,7 @@ tests += {
       'wait_cleanup',
       'heap_lock_update',
       'on_conflict_probe_window',
+      'multixact-aborted-updater',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/multixact-aborted-updater.spec b/src/test/modules/injection_points/specs/multixact-aborted-updater.spec
new file mode 100644
index 00000000000..4935aaa1f5f
--- /dev/null
+++ b/src/test/modules/injection_points/specs/multixact-aborted-updater.spec
@@ -0,0 +1,59 @@
+# Test multixact expansion with an aborted updater still in ProcArray.
+#
+# Pause session 1 after recording its abort in pg_xact, before ProcArray cleanup.
+# Session 3 can update without waiting, but expanding the multixact must discard
+# session 1 to avoid having two updating members.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE mxact_abort (id int PRIMARY KEY, filler text);
+	INSERT INTO mxact_abort VALUES (1, 'initial');
+}
+
+teardown
+{
+	DROP TABLE mxact_abort;
+	DROP EXTENSION injection_points;
+}
+
+session s1
+setup	{
+	SELECT FROM injection_points_set_local();
+	SELECT FROM injection_points_attach('transaction-abort-after-clog', 'wait');
+}
+step s1begin	{ BEGIN; }
+step s1update	{ UPDATE mxact_abort SET filler = 's1' WHERE id = 1; }
+step s1abort	{ ROLLBACK; }
+
+session s2
+# The compatible key-share lock makes xmax a multixact with session 1 as updater.
+step s2lock		{ SELECT * FROM mxact_abort WHERE id = 1 FOR KEY SHARE; }
+
+session s3
+# Omit variable XIDs from the error message.
+step s3update	{
+	DO $$
+	BEGIN
+		UPDATE mxact_abort SET filler = 's3' WHERE id = 1;
+		RAISE NOTICE 'session 3 update succeeded';
+	EXCEPTION WHEN OTHERS THEN
+		RAISE NOTICE 'session 3 update failed: %', split_part(SQLERRM, ':', 1);
+	END
+	$$;
+}
+step wake		{
+	SELECT FROM injection_points_detach('transaction-abort-after-clog');
+	SELECT FROM injection_points_wakeup('transaction-abort-after-clog');
+}
+
+permutation
+	s1begin
+	s1update
+	s2lock
+	# Pause after recording the abort, before ProcArray cleanup.
+	s1abort
+	# Expand the multixact without retaining the aborted updater.
+	s3update
+	wake
-- 
2.43.0

