From 0f6d9f7daf449ec2c1efdfacfffc4701560d4be5 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Sun, 15 Feb 2026 21:16:38 +0500
Subject: [PATCH 1/2] Test Multixact truncation near araparound

---
 src/bin/pg_resetwal/pg_resetwal.c             |  20 ++-
 src/test/modules/test_slru/Makefile           |   4 +-
 .../test_slru/t/002_multixact_wraparound.pl   | 165 ++++++++++++++++++
 src/test/modules/test_slru/test_multixact.c   |  54 ++++++
 src/test/modules/test_slru/test_slru--1.0.sql |   5 +
 5 files changed, 245 insertions(+), 3 deletions(-)
 create mode 100644 src/test/modules/test_slru/t/002_multixact_wraparound.pl
 create mode 100644 src/test/modules/test_slru/test_multixact.c

diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 6cb2bf47568..5f2b07c3038 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -73,6 +73,7 @@ static bool mxid_given = false;
 static MultiXactId set_mxid = 0;
 static bool mxoff_given = false;
 static MultiXactOffset set_mxoff = 0;
+static bool wal_level_replica = false;
 static TimeLineID minXlogTli = 0;
 static XLogSegNo minXlogSegNo = 0;
 static int	WalSegSz;
@@ -108,6 +109,7 @@ main(int argc, char *argv[])
 		{"oldest-transaction-id", required_argument, NULL, 'u'},
 		{"next-transaction-id", required_argument, NULL, 'x'},
 		{"wal-segsize", required_argument, NULL, 1},
+		{"wal-level", required_argument, NULL, 3},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -307,6 +309,19 @@ main(int argc, char *argv[])
 					break;
 				}
 
+			case 3:
+				if (pg_strcasecmp(optarg, "replica") == 0)
+					wal_level_replica = true;
+				else if (pg_strcasecmp(optarg, "minimal") == 0)
+					wal_level_replica = false;
+				else
+				{
+					pg_log_error("invalid argument for option %s", "--wal-level");
+					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+					exit(1);
+				}
+				break;
+
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -683,7 +698,7 @@ GuessControlValues(void)
 
 	/* minRecoveryPoint, backupStartPoint and backupEndPoint can be left zero */
 
-	ControlFile.wal_level = WAL_LEVEL_MINIMAL;
+	ControlFile.wal_level = wal_level_replica ? WAL_LEVEL_REPLICA : WAL_LEVEL_MINIMAL;
 	ControlFile.wal_log_hints = false;
 	ControlFile.track_commit_timestamp = false;
 	ControlFile.MaxConnections = 100;
@@ -890,7 +905,7 @@ RewriteControlFile(void)
 	 * as long as wal_level='minimal'; the postmaster will reset these fields
 	 * anyway at startup.
 	 */
-	ControlFile.wal_level = WAL_LEVEL_MINIMAL;
+	ControlFile.wal_level = wal_level_replica ? WAL_LEVEL_REPLICA : WAL_LEVEL_MINIMAL;
 	ControlFile.wal_log_hints = false;
 	ControlFile.track_commit_timestamp = false;
 	ControlFile.MaxConnections = 100;
@@ -1202,6 +1217,7 @@ usage(void)
 	printf(_("  -O, --multixact-offset=OFFSET    set next multitransaction offset\n"));
 	printf(_("  -u, --oldest-transaction-id=XID  set oldest transaction ID\n"));
 	printf(_("  -x, --next-transaction-id=XID    set next transaction ID\n"));
+	printf(_("      --wal-level=LEVEL            set checkpoint wal_level to \"minimal\" or \"replica\"\n"));
 	printf(_("      --wal-segsize=SIZE           size of WAL segments, in megabytes\n"));
 
 	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
diff --git a/src/test/modules/test_slru/Makefile b/src/test/modules/test_slru/Makefile
index 936886753b7..8870e49da85 100644
--- a/src/test/modules/test_slru/Makefile
+++ b/src/test/modules/test_slru/Makefile
@@ -3,7 +3,8 @@
 MODULE_big = test_slru
 OBJS = \
 	$(WIN32RES) \
-	test_slru.o
+	test_slru.o \
+	test_multixact.o
 PGFILEDESC = "test_slru - test module for SLRUs"
 
 EXTENSION = test_slru
@@ -11,6 +12,7 @@ DATA = test_slru--1.0.sql
 
 REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/test_slru/test_slru.conf
 REGRESS = test_slru
+TAP_TESTS = 1
 # Disabled because these tests require "shared_preload_libraries=test_slru",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/src/test/modules/test_slru/t/002_multixact_wraparound.pl b/src/test/modules/test_slru/t/002_multixact_wraparound.pl
new file mode 100644
index 00000000000..15db0656772
--- /dev/null
+++ b/src/test/modules/test_slru/t/002_multixact_wraparound.pl
@@ -0,0 +1,165 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test multixact SLRU truncation near wraparound with standby replay.
+# Creates an old multixact (mx 1) on heap, then pg_resetwal to advance next
+# multixact near wraparound.  VACUUM triggers truncation (TRUNCATE_ID WAL).
+# Standby must replay TRUNCATE_ID followed by CREATE_ID without crashing
+# (fixes bug where latest_page_number was incorrectly reset during truncation
+# replay).
+#
+# Uses backup_fs_cold + archive recovery (PITR) because the cold copy preserves
+# mx 1 (no autovacuum truncation before backup), but its checkpoint has
+# wal_level=minimal so streaming is impossible.  Archive all WAL and replay.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node_primary = PostgreSQL::Test::Cluster->new('main');
+$node_primary->init(
+	has_archiving => 1,
+	allows_streaming => 'physical',
+	auth_extra => [ '--create-role' => 'repl_role' ]);
+$node_primary->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'test_slru'");
+$node_primary->append_conf('postgresql.conf', qq[
+vacuum_multixact_freeze_min_age = 0
+vacuum_multixact_freeze_table_age = 0
+log_min_messages = debug1
+]);
+
+my $node_pgdata = $node_primary->data_dir;
+
+# Create old multixact (mx 1) on heap before pg_resetwal
+$node_primary->start;
+$node_primary->safe_psql('postgres', q(CREATE EXTENSION test_slru));
+$node_primary->safe_psql('postgres', q{
+	CREATE TABLE mx_trunc_tab (id int);
+	INSERT INTO mx_trunc_tab VALUES (1);
+});
+# FOR SHARE creates multixact 1 on heap xmax
+$node_primary->safe_psql('postgres', q{
+	BEGIN;
+	SELECT * FROM mx_trunc_tab FOR SHARE;
+	COMMIT;
+});
+# Create multixact 2 so mx 1's "next offset" is set (needed for GetMultiXactIdMembers)
+$node_primary->safe_psql('postgres', q{SELECT test_create_multixact();});
+
+$node_primary->stop;
+
+# Advance next multixact near wraparound; keep oldest=1 so mx 1 stays valid
+my $next_mx = 2**31;        # 2147483648, at wraparound boundary
+command_ok(
+	[
+		'pg_resetwal',
+		'--wal-level' => 'replica',
+		'--multixact-ids' => sprintf('%u,1', $next_mx),
+		$node_pgdata
+	],
+	"set next multixact to $next_mx (near wraparound), oldest to 1");
+
+# Extract values from pg_resetwal --dry-run for SLRU fixup
+my $out = (run_command([ 'pg_resetwal', '--dry-run', $node_primary->data_dir ]))[0];
+$out =~ /^Database block size: *(\d+)$/m or die "pg_resetwal output missing Database block size";
+my $blcksz = $1;
+# SLRU_PAGES_PER_SEGMENT is a compile-time constant (32) in slru.h; pg_resetwal doesn't output it
+my $slru_pages_per_segment = 32;
+
+# Create segment for next multixact; segment 0 with mx 1 stays for truncation
+my $multixact_offsets_per_page = $blcksz / 8;
+my $segno =
+  int($next_mx / $multixact_offsets_per_page / $slru_pages_per_segment);
+my $slru_file = sprintf('%s/pg_multixact/offsets/%04X', $node_pgdata, $segno);
+open my $fh, ">", $slru_file
+  or die "could not open \"$slru_file\": $!";
+binmode $fh;
+my $bytes_per_seg = $slru_pages_per_segment * $blcksz;
+syswrite($fh, "\0" x $bytes_per_seg) == $bytes_per_seg
+  or die "could not write to \"$slru_file\": $!";
+close $fh;
+
+# Cold copy preserves pg_resetwal state (mx 1 intact); checkpoint has
+# wal_level=minimal so use archive recovery instead of streaming.
+$node_primary->backup_fs_cold('mx_backup');
+
+# Start primary; it will archive WAL with wal_level=replica from config
+$node_primary->start;
+
+# mx 1 must be readable before truncation (segment 0 still exists)
+is( $node_primary->safe_psql('postgres', q{SELECT test_read_multixact('1');}),
+	'',
+	"multixact 1 readable before truncation");
+
+# Advance all databases' datminmxid so system-wide minimum allows truncation.
+# template0 has datallowconn=false by default; allow connections so vacuumdb
+# --all includes it (vacuumdb skips databases with datallowconn=false).
+# vacuum_multixact_freeze_min_age=0 makes MultiXactCutoff=nextMXID (2^31)
+# which exists (from template0 FOR SHARE), so truncation can succeed.
+$node_primary->safe_psql('postgres', q{ALTER DATABASE template0 WITH ALLOW_CONNECTIONS true});
+$node_primary->safe_psql('template0',
+	q{SELECT * FROM pg_catalog.pg_class LIMIT 1 FOR SHARE});
+# Vacuum all databases (including template0) so every relation's relminmxid advances past 1
+$node_primary->command_ok([ 'vacuumdb', '--all', '--freeze', '--port', $node_primary->port ],
+	'vacuumdb --all --freeze');
+$node_primary->safe_psql('postgres', q{ALTER DATABASE template0 WITH ALLOW_CONNECTIONS false});
+
+# CREATE_ID WAL records must follow TRUNCATE_ID - stresses latest_page_number fix.
+# Create enough multixacts to cross an offset page boundary (next-page bug): the
+# first multixact (2^31) is at entry 0 of its page, so we need offsets_per_page
+# more to fill the page, and one more to trigger allocation of the next page.
+my $multixacts_to_next_page = $multixact_offsets_per_page + 1;
+foreach my $i (1 .. $multixacts_to_next_page)
+{
+	$node_primary->safe_psql('postgres', q{SELECT test_create_multixact();});
+}
+
+# Force archive so standby can replay
+$node_primary->safe_psql('postgres', q{SELECT pg_switch_wal()});
+
+# Standby from cold backup, replay via archive (no streaming)
+my $node_standby = PostgreSQL::Test::Cluster->new('standby');
+$node_standby->init_from_backup($node_primary, 'mx_backup',
+	has_restoring => 1,
+	has_streaming => 0,
+	standby => 1);
+$node_standby->append_conf('postgresql.conf',
+	"log_min_messages = debug1\nwal_retrieve_retry_interval = '100ms'\nmax_connections = 100");
+$node_standby->start;
+
+my $primary_lsn = $node_primary->lsn('flush');
+$node_standby->poll_query_until('postgres',
+	qq{SELECT '$primary_lsn'::pg_lsn <= pg_last_wal_replay_lsn()})
+	or die "Timed out waiting for standby to replay";
+
+# Standby must replay truncation (from archive)
+my $standby_log = $node_standby->log_content();
+ok( $standby_log =~ /replaying multixact truncation/,
+	"standby replayed multixact TRUNCATE_ID (truncation near wraparound)");
+
+# Multixact that crossed offset page boundary must be readable (next-page bug)
+my $multi_at_page_boundary = $next_mx + $multixact_offsets_per_page;
+is( $node_standby->safe_psql('postgres', qq{SELECT test_read_multixact('$multi_at_page_boundary');}),
+	'',
+	"multixact at offset page boundary readable on standby (next-page replay)");
+
+# New multixacts must be readable on standby
+my $first_new_multi = $node_primary->safe_psql('postgres',
+	q{SELECT test_create_multixact();});
+$node_primary->safe_psql('postgres', q{SELECT pg_switch_wal()});
+my $final_lsn = $node_primary->lsn('flush');
+$node_standby->poll_query_until('postgres',
+	qq{SELECT '$final_lsn'::pg_lsn <= pg_last_wal_replay_lsn()})
+	or die "Timed out waiting for standby to replay";
+is( $node_standby->safe_psql('postgres', qq{SELECT test_read_multixact('$first_new_multi');}),
+	'',
+	"new multixact readable on standby after truncation replay");
+
+$node_standby->stop;
+$node_primary->stop;
+
+done_testing();
diff --git a/src/test/modules/test_slru/test_multixact.c b/src/test/modules/test_slru/test_multixact.c
new file mode 100644
index 00000000000..7b961668116
--- /dev/null
+++ b/src/test/modules/test_slru/test_multixact.c
@@ -0,0 +1,54 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_multixact.c
+ *		Support code for multixact testing
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_slru/test_multixact.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/multixact.h"
+#include "access/xact.h"
+#include "fmgr.h"
+
+PG_FUNCTION_INFO_V1(test_create_multixact);
+PG_FUNCTION_INFO_V1(test_read_multixact);
+
+/*
+ * Produces multixact with 2 current xids
+ */
+Datum
+test_create_multixact(PG_FUNCTION_ARGS)
+{
+	MultiXactId id;
+
+	MultiXactIdSetOldestMember();
+	id = MultiXactIdCreate(GetCurrentTransactionId(), MultiXactStatusUpdate,
+						   GetCurrentTransactionId(), MultiXactStatusForShare);
+	PG_RETURN_TRANSACTIONID(id);
+}
+
+/*
+ * Reads given multixact.  Discards local cache to make a real read.
+ */
+Datum
+test_read_multixact(PG_FUNCTION_ARGS)
+{
+	MultiXactId id = PG_GETARG_TRANSACTIONID(0);
+	MultiXactMember *members;
+
+	/* discard caches */
+	AtEOXact_MultiXact();
+
+	if (GetMultiXactIdMembers(id, &members, false, false) == -1)
+		elog(ERROR, "MultiXactId not found");
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_slru/test_slru--1.0.sql b/src/test/modules/test_slru/test_slru--1.0.sql
index 202e8da3fde..a7c6c458a5e 100644
--- a/src/test/modules/test_slru/test_slru--1.0.sql
+++ b/src/test/modules/test_slru/test_slru--1.0.sql
@@ -19,3 +19,8 @@ CREATE OR REPLACE FUNCTION test_slru_page_truncate(bigint) RETURNS VOID
   AS 'MODULE_PATHNAME', 'test_slru_page_truncate' LANGUAGE C;
 CREATE OR REPLACE FUNCTION test_slru_delete_all() RETURNS VOID
   AS 'MODULE_PATHNAME', 'test_slru_delete_all' LANGUAGE C;
+
+CREATE OR REPLACE FUNCTION test_create_multixact() RETURNS xid
+  AS 'MODULE_PATHNAME', 'test_create_multixact' LANGUAGE C;
+CREATE OR REPLACE FUNCTION test_read_multixact(xid) RETURNS VOID
+  AS 'MODULE_PATHNAME', 'test_read_multixact' LANGUAGE C;
-- 
2.51.2

