From 3b2f1bcea74c8523756b66c49a0b92e387e3e02d Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Fri, 24 Jul 2026 08:18:54 +0000
Subject: [PATCH] Propagate rebalanced cost limit to parallel vacuum workers

AutoVacuumUpdateCostLimit() runs after each nap in vacuum_delay_point()
and follows av_nworkersForBalance, but the new limit never reached the
shared cost params in the vacuum DSM: propagation only happened on
config reload. Parallel workers computed their delays from the stale
limit, so a parallel autovacuum could run at up to twice the configured
budget (or half of it) until the next SIGHUP, contradicting the
propagation promise in maintenance.sgml.

Call parallel_vacuum_propagate_shared_delay_params() after rebalancing.
Gated on the leader: parallel workers take the same nap path and must
not overwrite the shared parameters.

Add a test: pause the leader at the existing injection point, start a
second autovacuum worker, check the first parameter load of the
parallel workers reports the balanced limit.
---
 src/backend/commands/vacuum.c                 |   3 +
 src/test/modules/test_autovacuum/meson.build  |   1 +
 .../test_autovacuum/t/002_cost_rebalance.pl   | 113 ++++++++++++++++++
 3 files changed, 117 insertions(+)
 create mode 100644 src/test/modules/test_autovacuum/t/002_cost_rebalance.pl

diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..c17aecce7ad 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2577,6 +2577,9 @@ vacuum_delay_point(bool is_analyze)
 		 */
 		AutoVacuumUpdateCostLimit();
 
+		if (AmAutoVacuumWorkerProcess())
+			parallel_vacuum_propagate_shared_delay_params();
+
 		/* Might have gotten an interrupt while sleeping */
 		CHECK_FOR_INTERRUPTS();
 	}
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
index 86e392bc0de..7fbe50daa28 100644
--- a/src/test/modules/test_autovacuum/meson.build
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -10,6 +10,7 @@ tests += {
     },
     'tests': [
       't/001_parallel_autovacuum.pl',
+      't/002_cost_rebalance.pl',
     ],
   },
 }
diff --git a/src/test/modules/test_autovacuum/t/002_cost_rebalance.pl b/src/test/modules/test_autovacuum/t/002_cost_rebalance.pl
new file mode 100644
index 00000000000..0089d9af780
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/002_cost_rebalance.pl
@@ -0,0 +1,113 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test that cost limit rebalancing reaches parallel autovacuum workers.
+#
+# Leader pauses at the injection point after the shared cost param snapshot
+# (balance = 1, limit 200). A second autovacuum worker starts (balance = 2,
+# limit 100). After resume, the parallel workers' first param load must show
+# the rebalanced 100, not the snapshotted 200.
+
+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', qq{
+autovacuum_naptime = '1s'
+autovacuum_max_workers = 3
+autovacuum_worker_slots = 4
+autovacuum_max_parallel_workers = 2
+autovacuum_vacuum_cost_delay = '20ms'
+vacuum_cost_limit = 200
+max_worker_processes = 16
+max_parallel_workers = 8
+log_min_messages = debug2
+min_parallel_index_scan_size = 0
+log_autovacuum_min_duration = -1
+});
+$node->start;
+
+if (!$node->check_extension('injection_points'))
+{
+	plan skip_all => 'Extension injection_points not installed';
+}
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points');
+$node->safe_psql('postgres', 'CREATE DATABASE regress_db2');
+
+# leader's table
+$node->safe_psql(
+	'postgres', qq{
+	CREATE TABLE test_av (id serial primary key, col_1 int, col_2 int)
+	  WITH (autovacuum_parallel_workers = 2, autovacuum_enabled = false);
+	INSERT INTO test_av SELECT g, g + 1, g + 2 FROM generate_series(1, 50000) g;
+	CREATE INDEX test_av_col_1 ON test_av (col_1);
+	CREATE INDEX test_av_col_2 ON test_av (col_2);
+});
+
+# second worker's table: no indexes, no cost reloptions (participates in
+# balancing), sized to outlast the test
+$node->safe_psql(
+	'regress_db2', qq{
+	CREATE TABLE filler (id int, pad text) WITH (autovacuum_enabled = false);
+	INSERT INTO filler SELECT g, repeat('x', 100) FROM generate_series(1, 200000) g;
+});
+
+# quiesce catalogs so no extra worker skews the balance
+$node->safe_psql($_, 'VACUUM ANALYZE')
+  for ('postgres', 'regress_db2', 'template1');
+
+my $db2oid = $node->safe_psql('postgres',
+	"SELECT oid FROM pg_database WHERE datname = 'regress_db2'");
+my $filleroid = $node->safe_psql('regress_db2', "SELECT 'filler'::regclass::oid");
+
+$node->safe_psql('postgres', 'UPDATE test_av SET col_1 = col_1 + 1');
+$node->safe_psql('regress_db2', 'UPDATE filler SET id = id + 1');
+
+my $log_offset = -s $node->logfile;
+
+# pause leader after the shared cost param snapshot
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('autovacuum-start-parallel-vacuum', 'wait')");
+$node->safe_psql('postgres', 'ALTER TABLE test_av SET (autovacuum_enabled = true)');
+$node->wait_for_event('autovacuum worker', 'autovacuum-start-parallel-vacuum');
+
+# second worker -> balance = 2
+$node->safe_psql('regress_db2', 'ALTER TABLE filler SET (autovacuum_enabled = true)');
+$node->wait_for_log(
+	qr/VacuumUpdateCosts\(db=$db2oid, rel=$filleroid, dobalance=yes, cost_limit=100,/,
+	$log_offset);
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('autovacuum-start-parallel-vacuum')");
+$node->safe_psql('postgres',
+	"SELECT injection_points_detach('autovacuum-start-parallel-vacuum')");
+
+# first param load must show the rebalanced limit
+$node->wait_for_log(
+	qr/parallel autovacuum worker updated cost params: cost_limit=\d+,/,
+	$log_offset);
+my $log = slurp_file($node->logfile, $log_offset);
+my @limits =
+  $log =~ /parallel autovacuum worker updated cost params: cost_limit=(\d+),/g;
+note("parallel worker cost_limit sequence: @limits");
+is($limits[0], '100',
+	'parallel workers see the rebalanced cost limit');
+
+my $filler_running = $node->safe_psql('regress_db2',
+	"SELECT count(*) FROM pg_stat_progress_vacuum WHERE relid = 'filler'::regclass");
+is($filler_running, '1', 'second autovacuum worker was still running');
+
+$node->stop;
+done_testing();
-- 
2.54.0

