From 1fab3624306046e33719ee15d6d719f87b565608 Mon Sep 17 00:00:00 2001
From: shihao zhong <zhong950419@gmail.com>
Date: Wed, 28 Oct 2026 23:12:31 -0400
Subject: [PATCH 2/2] Add test for checkpoint during multixact creation

Adds an injection point after the CREATE_ID record, and a crash test
in test_slru.

Backpatch-through: 18
---
 src/backend/access/transam/multixact.c        |  5 +-
 src/test/modules/test_slru/meson.build        |  3 +-
 .../test_slru/t/003_multixact_checkpoint.pl   | 84 +++++++++++++++++++
 3 files changed, 90 insertions(+), 2 deletions(-)
 create mode 100644 src/test/modules/test_slru/t/003_multixact_checkpoint.pl

diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index e75c5238295..9732388e1d3 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -755,8 +755,9 @@ MultiXactIdCreateFromMembers(int nmembers, MultiXactMember *members)
 		}
 	}
 
-	/* Load the injection point before entering the critical section */
+	/* Load the injection points before entering the critical section */
 	INJECTION_POINT_LOAD("multixact-create-from-members");
+	INJECTION_POINT_LOAD("multixact-create-after-wal");
 
 	/*
 	 * Assign the MXID and offsets range to use, and make sure there is space
@@ -801,6 +802,8 @@ MultiXactIdCreateFromMembers(int nmembers, MultiXactMember *members)
 
 	(void) XLogInsert(RM_MULTIXACT_ID, XLOG_MULTIXACT_CREATE_ID);
 
+	INJECTION_POINT_CACHED("multixact-create-after-wal", NULL);
+
 	/* Now enter the information into the OFFSETs and MEMBERs logs */
 	RecordNewMultiXact(multi, offset, nmembers, members);
 
diff --git a/src/test/modules/test_slru/meson.build b/src/test/modules/test_slru/meson.build
index 00f3ee3054d..57cc8b04943 100644
--- a/src/test/modules/test_slru/meson.build
+++ b/src/test/modules/test_slru/meson.build
@@ -39,7 +39,8 @@ tests += {
     },
     'tests': [
       't/001_multixact.pl',
-      't/002_multixact_wraparound.pl'
+      't/002_multixact_wraparound.pl',
+      't/003_multixact_checkpoint.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_slru/t/003_multixact_checkpoint.pl b/src/test/modules/test_slru/t/003_multixact_checkpoint.pl
new file mode 100644
index 00000000000..72ffa07b082
--- /dev/null
+++ b/src/test/modules/test_slru/t/003_multixact_checkpoint.pl
@@ -0,0 +1,84 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test that a checkpoint cannot start between the WAL record of a new
+# multixact and the update of the offsets and members SLRUs.  If it could,
+# the checkpoint might flush the SLRU pages before they hold the new
+# multixact, and crash recovery would begin after the WAL record, losing
+# the multixact's members.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'test_slru,injection_points'");
+$node->start;
+$node->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+$node->safe_psql('postgres', q(CREATE EXTENSION test_slru));
+
+# The multixact created below gets the next multixact ID.
+$node->safe_psql('postgres', q(CHECKPOINT));
+my $multi = $node->safe_psql('postgres',
+	q(SELECT next_multixact_id FROM pg_control_checkpoint()));
+my $ckpt_lsn = $node->safe_psql('postgres',
+	q(SELECT checkpoint_lsn FROM pg_control_checkpoint()));
+
+# Create a multixact, and stop after its WAL record has been inserted.
+my $creator = $node->background_psql('postgres');
+$creator->query_safe(
+	q(SELECT injection_points_set_local();
+SELECT injection_points_attach('multixact-create-after-wal', 'wait');));
+$creator->query_until(
+	qr/creating multi/, q(
+\echo creating multi
+SELECT test_create_multixact();
+));
+$node->wait_for_event('client backend', 'multixact-create-after-wal');
+
+# Request a checkpoint.  Its redo pointer is after the multixact's WAL
+# record.  The checkpoint must wait for the creator to finish.
+my $checkpoint = $node->background_psql('postgres');
+$checkpoint->query_until(
+	qr/starting checkpoint/, q(
+\echo starting checkpoint
+CHECKPOINT;
+));
+
+# Wait until the checkpoint has either completed or is waiting for the
+# creator.
+$node->poll_query_until(
+	'postgres', qq(
+SELECT checkpoint_lsn <> '$ckpt_lsn' FROM pg_control_checkpoint()
+UNION ALL
+SELECT true FROM pg_stat_activity
+  WHERE backend_type = 'checkpointer'
+  AND wait_event = 'CheckpointDelayStart'
+ORDER BY 1 DESC LIMIT 1))
+  or die "timed out waiting for the checkpoint";
+
+$node->safe_psql('postgres',
+	q(SELECT injection_points_wakeup('multixact-create-after-wal')));
+$creator->quit;
+$checkpoint->quit;
+
+# Crash, and check that the members of the multixact survived.
+$node->stop('immediate');
+$node->start;
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres',
+	qq(SELECT count(*) FILTER (WHERE xid <> '0') FROM pg_get_multixact_members('$multi')));
+is($ret, 0, 'multixact members can be read after crash');
+is($stdout, '2', 'multixact members are valid after crash');
+
+done_testing();
-- 
2.37.1 (Apple Git-137.1)

