From 21df920e307eb5d534c0cb3459824ee6e82d6a31 Mon Sep 17 00:00:00 2001 From: Zhong ShiHao Date: Mon, 31 Aug 2026 20:42:44 -0400 Subject: [PATCH] Don't let a larger autovacuum freeze weight lower the freeze score Raising a freeze weight lowers the age at which the score starts scaling, but the score is scaled by raising it to a power, which shrinks it when it is below one. A weight above 1.0 could thus collapse the score of a table close to wraparound, demoting it below younger tables. Apply the exponent only when it increases the score, for both components. Add a test that fakes an old relfrozenxid/relminmxid (safe: the score reads them only arithmetically, never via clog) and checks neither score drops as its weight grows. New in v19 (d7965d65fc5), not back-patched. --- src/backend/postmaster/autovacuum.c | 10 ++- src/test/modules/test_autovacuum/meson.build | 1 + .../test_autovacuum/t/003_freeze_score.pl | 85 +++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/test/modules/test_autovacuum/t/003_freeze_score.pl diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 393e81d53e9..480f16b5c74 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -3210,9 +3210,15 @@ relation_needs_vacanalyze(Oid relid, if (autovacuum_multixact_freeze_score_weight > 1.0) effective_mxid_failsafe_age /= autovacuum_multixact_freeze_score_weight; - if (xid_age >= effective_xid_failsafe_age) + /* + * Only apply the exponent when it increases the score. Dividing the + * effective ages by the weights above can let a score below one reach this + * point, and raising that to a power shrinks it, so a larger weight could + * otherwise demote a table that is closer to wraparound. + */ + if (xid_age >= effective_xid_failsafe_age && scores->xid > 1.0) scores->xid = pow(scores->xid, Max(1.0, (double) xid_age / 100000000)); - if (mxid_age >= effective_mxid_failsafe_age) + if (mxid_age >= effective_mxid_failsafe_age && scores->mxid > 1.0) scores->mxid = pow(scores->mxid, Max(1.0, (double) mxid_age / 100000000)); scores->xid *= autovacuum_freeze_score_weight; diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build index 970b9aaae4b..56601789960 100644 --- a/src/test/modules/test_autovacuum/meson.build +++ b/src/test/modules/test_autovacuum/meson.build @@ -11,6 +11,7 @@ tests += { 'tests': [ 't/001_parallel_autovacuum.pl', 't/002_toast_relopts.pl', + 't/003_freeze_score.pl', ], }, } diff --git a/src/test/modules/test_autovacuum/t/003_freeze_score.pl b/src/test/modules/test_autovacuum/t/003_freeze_score.pl new file mode 100644 index 00000000000..7769ea1fb07 --- /dev/null +++ b/src/test/modules/test_autovacuum/t/003_freeze_score.pl @@ -0,0 +1,85 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Raising an autovacuum freeze weight must never lower the corresponding +# freeze score, for both the transaction ID and multixact components. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; + +# Keep the *_freeze_max_age parameters well above the faked ages so the table +# is not force-vacuumed for wraparound, and drop the failsafe ages to their +# minimum so *_freeze_max_age alone governs the scaling threshold. +$node->append_conf( + 'postgresql.conf', qq[ +autovacuum = off +autovacuum_freeze_max_age = 1000000000 +autovacuum_multixact_freeze_max_age = 1000000000 +vacuum_failsafe_age = 0 +vacuum_multixact_failsafe_age = 0 +]); +$node->start; + +$node->safe_psql('postgres', + 'CREATE TABLE freezetest (i int) WITH (autovacuum_enabled = off)'); + +# Fake old relfrozenxid/relminmxid rather than consuming hundreds of millions +# of transactions. The score view only reads these fields arithmetically +# (recentXid - relfrozenxid, recentMulti - relminmxid, both modulo 2^32) and +# never consults clog for them, and with autovacuum off and the faked ages +# below *_freeze_max_age nothing else acts on them, so this is safe. There is +# no SQL primitive for the next multixact, but on a freshly initialized node it +# is near the next xid, so subtracting from pg_snapshot_xmax() yields a usable +# multixact age too; the assertions below confirm both ages landed in range. +my $age = 300_000_000; +$node->safe_psql( + 'postgres', qq[ +UPDATE pg_class + SET relfrozenxid = ((pg_snapshot_xmax(pg_current_snapshot())::text::bigint + - $age + 4294967296) % 4294967296)::text::xid, + relminmxid = ((pg_snapshot_xmax(pg_current_snapshot())::text::bigint + - $age + 4294967296) % 4294967296)::text::xid + WHERE relname = 'freezetest']); + +my ($xid_age, $mxid_age) = split /\|/, $node->safe_psql('postgres', + q[SELECT age(relfrozenxid) || '|' || mxid_age(relminmxid) + FROM pg_class WHERE relname = 'freezetest']); +cmp_ok($xid_age, '>', 100_000_000, 'xid age reaches the scaled regime'); +cmp_ok($mxid_age, '>', 100_000_000, 'mxid age reaches the scaled regime'); + +# Weights near 3.5 are where the unguarded code collapsed the score. +my (%prev); +foreach my $weight (1.0, 2.0, 3.0, 3.5, 5.0, 10.0) +{ + foreach my $kind (qw(freeze multixact_freeze)) + { + $node->safe_psql('postgres', + "ALTER SYSTEM SET autovacuum_${kind}_score_weight = $weight"); + } + $node->reload; + $node->poll_query_until('postgres', + "SELECT current_setting('autovacuum_freeze_score_weight')::float8 = $weight" + ) or die "timed out waiting for weight $weight"; + + my ($xid, $mxid) = split /\|/, $node->safe_psql('postgres', + q[SELECT xid_score || '|' || mxid_score + FROM pg_stat_autovacuum_scores WHERE relname = 'freezetest']); + + foreach my $c (['xid', $xid], ['mxid', $mxid]) + { + my ($name, $score) = @$c; + cmp_ok($score, '>', 0, "$name score positive at weight $weight"); + cmp_ok($score, '>=', $prev{$name}, + "$name score does not decrease at weight $weight") + if defined $prev{$name}; + $prev{$name} = $score; + } +} + +$node->stop; +done_testing(); -- 2.37.1 (Apple Git-137.1)