From 6f7386b5a86cd2ea12ac1e8b7e8fb3052c4fcee4 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 v3] 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 and hold it at a new injection point placed
after it joined the balance, then check the first parameter load of the
parallel workers reports the balanced limit. The hold is needed because
a second worker left running can finish its own vacuum before the leader
resumes, which puts the balance back where it started.
---
 src/backend/commands/vacuum.c                 |  3 +
 src/backend/postmaster/autovacuum.c           |  1 +
 .../t/001_parallel_autovacuum.pl              | 82 ++++++++++++++++++-
 3 files changed, 85 insertions(+), 1 deletion(-)

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/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 45abf48768a..b06da6bce1d 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2487,6 +2487,7 @@ do_autovacuum(void)
 		 */
 		VacuumUpdateCosts();
 
+		INJECTION_POINT("autovacuum-worker-cost-balanced", NULL);
 
 		/* clean up memory before each iteration */
 		MemoryContextReset(PortalContext);
diff --git a/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl
index 22f40cb1d50..ca7ff727b5e 100644
--- a/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl
+++ b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl
@@ -36,7 +36,7 @@ $node->init;
 $node->append_conf(
 	'postgresql.conf', qq{
 autovacuum_max_workers = 1
-autovacuum_worker_slots = 1
+autovacuum_worker_slots = 2
 autovacuum_max_parallel_workers = 2
 max_worker_processes = 10
 max_parallel_workers = 10
@@ -167,5 +167,85 @@ ok(1,
 	"vacuum delay parameter changes are propagated to parallel vacuum workers"
 );
 
+# Test 3:
+# Check whether a cost limit rebalance reaches the parallel workers. The
+# leader pauses right after taking the shared cost param snapshot
+# (balance = 1, limit 500), then a second autovacuum worker joins the
+# balance (balance = 2, limit 250) and is held there for the rest of the
+# test. After resume, the parallel workers' first parameter load must show
+# the rebalanced 250, not the snapshotted 500.
+
+# Second worker's table lives in another database: no cost reloptions, so it
+# participates in balancing.
+$node->safe_psql('postgres', 'CREATE DATABASE regress_db2');
+$node->safe_psql(
+	'regress_db2', qq{
+	CREATE TABLE filler (id int) WITH (autovacuum_enabled = false);
+	INSERT INTO filler SELECT g FROM generate_series(1, 1000) g;
+});
+
+# Allow a second autovacuum worker.
+$node->safe_psql(
+	'postgres', qq{
+	ALTER SYSTEM SET autovacuum_max_workers = 2;
+	SELECT pg_reload_conf();
+});
+
+# Quiesce catalogs so no extra worker skews the balance.
+$node->safe_psql($_, 'VACUUM ANALYZE')
+  for ('postgres', 'regress_db2', 'template1');
+
+prepare_for_next_test($node, 3);
+$node->safe_psql('regress_db2', 'UPDATE filler SET id = id + 1');
+
+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");
+
+$log_offset = -s $node->logfile;
+
+# Pause the leader after the shared cost param snapshot. The leader is past
+# the hold point below by then, so that one only catches the second worker.
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('autovacuum-start-parallel-vacuum', 'wait')"
+);
+$node->safe_psql('postgres',
+	'ALTER TABLE test_autovac SET (autovacuum_enabled = true)');
+$node->wait_for_event('autovacuum worker',
+	'autovacuum-start-parallel-vacuum');
+
+# Second worker -> balance = 2. Hold it there: if it were allowed to finish,
+# the balance would drop back to 1 before the leader resumes.
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('autovacuum-worker-cost-balanced', 'wait')"
+);
+$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=250,/,
+	$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], '250', 'parallel workers see the rebalanced cost limit');
+
+# Release the second worker.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('autovacuum-worker-cost-balanced')");
+$node->safe_psql('postgres',
+	"SELECT injection_points_detach('autovacuum-worker-cost-balanced')");
+
 $node->stop;
 done_testing();
-- 
2.55.0

