From e8e794b0ef1c2165081cc0ad87edfe16c40badd1 Mon Sep 17 00:00:00 2001 From: Manuel Reyes Bravo Date: Sat, 17 Oct 2026 06:00:00 -0300 Subject: [PATCH v3] pg_upgrade: Don't store an invalid datminmxid/relminmxid If the old cluster's newest multixid is the last one before multixid wraparound (0xFFFFFFFF), the next multixid in its control file is 0: before v19, the server skips 0 only when it assigns the next multixid. set_frozenxids() copied that 0 into pg_database.datminmxid and into pg_class.relminmxid of the relations that pg_upgrade does not restore from the old cluster, i.e. the catalogs created by initdb, in every database. 0 is InvalidMultiXactId. On assert-enabled builds, the next VACUUM fails an assertion. Otherwise template0 keeps datminmxid = 0, for which mxid_age() reports 2147483647, until autovacuum processes it; with autovacuum = off, it stays that way. Use FirstMultiXactId instead. In v19, copy_xact_xlog_xid() already does the same for the next multixid it passes to pg_resetwal when converting multixacts. This is not new in v19. Before commit 8747b969fd8, pg_upgrade to v15-v18 failed on such a cluster, because pg_resetwal refused "-m 0"; since then, the upgrade succeeds and stores the invalid values. v14 did not get that commit, so pg_upgrade to v14 still fails on such a cluster and is not affected. Since commit 87a350e1f28, v19 servers never store 0 as the next multixid, so only pre-v19 old clusters are affected. Backpatch-through: 15 --- src/bin/pg_upgrade/pg_upgrade.c | 16 +++- .../pg_upgrade/t/003_multixid_wraparound.pl | 91 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/bin/pg_upgrade/t/003_multixid_wraparound.pl diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c index 265d8294906..15a33e5af1e 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -44,6 +44,7 @@ #include #endif +#include "access/multixact.h" #include "catalog/pg_class_d.h" #include "common/file_perm.h" #include "common/logging.h" @@ -697,12 +698,23 @@ set_frozenxids(bool minmxid_only) int ntups; int i_datname; int i_datallowconn; + MultiXactId minmxid; if (!minmxid_only) prep_status("Setting frozenxid and minmxid counters in new cluster"); else prep_status("Setting minmxid counter in new cluster"); + /* + * If the old cluster's newest multixid was the last one before + * wraparound, its next multixid is 0: the server skips 0 only when it + * assigns the next multixid. 0 is not a valid datminmxid or + * relminmxid, so use FirstMultiXactId, the next multixid assigned. + */ + minmxid = old_cluster.controldata.chkpnt_nxtmulti; + if (minmxid < FirstMultiXactId) + minmxid = FirstMultiXactId; + conn_template1 = connectToServer(&new_cluster, "template1"); if (!minmxid_only) @@ -716,7 +728,7 @@ set_frozenxids(bool minmxid_only) PQclear(executeQueryOrDie(conn_template1, "UPDATE pg_catalog.pg_database " "SET datminmxid = '%u'", - old_cluster.controldata.chkpnt_nxtmulti)); + minmxid)); /* get database names */ dbres = executeQueryOrDie(conn_template1, @@ -767,7 +779,7 @@ set_frozenxids(bool minmxid_only) CppAsString2(RELKIND_RELATION) ", " CppAsString2(RELKIND_MATVIEW) ", " CppAsString2(RELKIND_TOASTVALUE) ")", - old_cluster.controldata.chkpnt_nxtmulti)); + minmxid)); PQfinish(conn); /* Reset datallowconn flag */ diff --git a/src/bin/pg_upgrade/t/003_multixid_wraparound.pl b/src/bin/pg_upgrade/t/003_multixid_wraparound.pl new file mode 100644 index 00000000000..4ebd45722ed --- /dev/null +++ b/src/bin/pg_upgrade/t/003_multixid_wraparound.pl @@ -0,0 +1,91 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Upgrade a cluster whose newest multixid is the last one before +# multixid wraparound (0xFFFFFFFF), so that the next multixid in its +# control file is 0. That must not end up as datminmxid or relminmxid +# in the new cluster. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Run pg_upgrade in the build directory, see 002_pg_upgrade.pl. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +my $old = PostgreSQL::Test::Cluster->new('old_node'); +my $new = PostgreSQL::Test::Cluster->new('new_node'); + +$old->init; +$old->append_conf('postgresql.conf', 'max_prepared_transactions = 2'); + +$old->start; +my $blcksz = $old->safe_psql('postgres', 'SHOW block_size'); +$old->stop; + +# Set the next multixid to 0xFFFFFFFF. The server refuses to start if +# the 'offsets' SLRU segment covering it doesn't exist, so replace the +# segments with one that covers it, filled with zeros. +command_ok( + [ 'pg_resetwal', '-m', '0xFFFFFFFF,0xFFFFFFFF', $old->data_dir ], + 'set next multixid to 0xFFFFFFFF'); +unlink glob $old->data_dir . "/pg_multixact/offsets/*"; +my $segsize = 32 * $blcksz; +my $path = sprintf('%s/pg_multixact/offsets/%04X', + $old->data_dir, int(0xFFFFFFFF / ($blcksz / 4) / 32)); +open my $fh, '>', $path or die "could not open \"$path\": $!"; +binmode $fh; +syswrite($fh, "\0" x $segsize) == $segsize + or die "could not write to \"$path\": $!"; +close $fh; + +# Create one multixid: two prepared transactions holding KEY SHARE locks +# on the same row. +$old->start; +$old->safe_psql( + 'postgres', q[ + CREATE TABLE mxtest (id int PRIMARY KEY) WITH (autovacuum_enabled = false); + INSERT INTO mxtest VALUES (1); + BEGIN; SELECT * FROM mxtest FOR KEY SHARE; PREPARE TRANSACTION 'mx1'; + BEGIN; SELECT * FROM mxtest FOR KEY SHARE; PREPARE TRANSACTION 'mx2'; + COMMIT PREPARED 'mx1'; + COMMIT PREPARED 'mx2'; +]); +$old->stop; + +command_like( + [ 'pg_controldata', $old->data_dir ], + qr/^Latest checkpoint's NextMultiXactId:\s*0$/m, + 'next multixid wrapped around to 0 in old cluster'); + +$new->init; +command_ok( + [ + 'pg_upgrade', '--no-sync', + '-d', $old->data_dir, + '-D', $new->data_dir, + '-b', $old->config_data('--bindir'), + '-B', $new->config_data('--bindir'), + '-s', $new->host, + '-p', $old->port, + '-P', $new->port + ], + 'run of pg_upgrade'); + +$new->start; +is( $new->safe_psql( + 'postgres', "SELECT count(*) FROM pg_database WHERE datminmxid = '0'"), + '0', + 'no database has an invalid datminmxid after upgrade'); +is( $new->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_class WHERE relminmxid = '0' AND relkind IN ('r', 'm', 't')" + ), + '0', + 'no table has an invalid relminmxid after upgrade'); +$new->safe_psql('postgres', 'VACUUM'); +$new->stop; + +done_testing(); -- 2.56.0