From 02c84ee03e49bfd07b3be4ff23719c4eb3b8c461 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 | 15 ++++- .../pg_upgrade/t/007_multixact_conversion.pl | 59 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c index 2127d297bfe..2dc30515c1c 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -934,12 +934,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: servers before v19 skip 0 only + * when they assign the next multixid. 0 is not a valid datminmxid or + * relminmxid, so use FirstMultiXactId, as copy_xact_xlog_xid() does. + */ + minmxid = old_cluster.controldata.chkpnt_nxtmulti; + if (minmxid < FirstMultiXactId) + minmxid = FirstMultiXactId; + conn_template1 = connectToServer(&new_cluster, "template1"); if (!minmxid_only) @@ -953,7 +964,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, @@ -1004,7 +1015,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/007_multixact_conversion.pl b/src/bin/pg_upgrade/t/007_multixact_conversion.pl index 5c87c2527ee..4cc7e1bc9ad 100644 --- a/src/bin/pg_upgrade/t/007_multixact_conversion.pl +++ b/src/bin/pg_upgrade/t/007_multixact_conversion.pl @@ -484,4 +484,63 @@ SKIP: upgrade_and_compare($tag, $old, $new); } +# Multixid wraparound at the last multixid: The newest multixid is +# 0xFFFFFFFF, so the old cluster's next multixid is 0. That must not +# end up as datminmxid or relminmxid in the new cluster. +SKIP: +{ + skip + "skipping mxid wraparound tests because upgrading from the old version does not require conversion" + if ($old_version >= '19devel'); + + my $tag = 'last_mxid'; + my $old = + PostgreSQL::Test::Cluster->new("${tag}_oldnode", + install_path => $ENV{oldinstall}); + my $new = PostgreSQL::Test::Cluster->new("${tag}_newnode"); + + $old->init(extra => ['-k']); + + reset_mxid_mxoffset_pre_v19($old, 0xFFFFFFFF, 0x10000); + + # Create one multixid with two members, a locker and an updater + $old->start; + $old->safe_psql( + 'postgres', qq[ + CREATE TABLE mxofftest (id INT PRIMARY KEY, n_updated INT) + WITH (AUTOVACUUM_ENABLED=FALSE); + INSERT INTO mxofftest SELECT G, 0 FROM GENERATE_SERIES(1, 2) G; + ]); + my $locker = + $new->background_psql('', connstr => $old->connstr('postgres')); + $locker->query_safe("BEGIN"); + $locker->query_safe("SELECT * FROM mxofftest WHERE id = 1 FOR KEY SHARE"); + $old->safe_psql('postgres', + "UPDATE mxofftest SET n_updated = n_updated + 1 WHERE id = 1"); + $locker->query_safe("COMMIT"); + $locker->quit; + $old->stop; + + my (undef, $next_mxid, undef) = read_multixid_fields($old); + is($next_mxid, 0, "next multixid wrapped around to 0 in old cluster"); + + $new->init; + upgrade_and_compare($tag, $old, $new); + + $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