v20-0004-Tests-for-parallel-autovacuum.patch
text/x-patch
Filename: v20-0004-Tests-for-parallel-autovacuum.patch
Type: text/x-patch
Part: 1
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v20-0004
Subject: Tests for parallel autovacuum
| File | + | − |
|---|---|---|
| src/backend/commands/vacuumparallel.c | 60 | 0 |
| src/backend/postmaster/autovacuum.c | 19 | 0 |
| src/include/postmaster/autovacuum.h | 1 | 0 |
| src/test/modules/Makefile | 1 | 0 |
| src/test/modules/meson.build | 1 | 0 |
| src/test/modules/test_autovacuum/.gitignore | 2 | 0 |
| src/test/modules/test_autovacuum/Makefile | 28 | 0 |
| src/test/modules/test_autovacuum/meson.build | 36 | 0 |
| src/test/modules/test_autovacuum/t/001_basic.pl | 346 | 0 |
| src/test/modules/test_autovacuum/test_autovacuum--1.0.sql | 12 | 0 |
| src/test/modules/test_autovacuum/test_autovacuum.c | 41 | 0 |
| src/test/modules/test_autovacuum/test_autovacuum.control | 3 | 0 |
From 411d69b72072316f1e499a59034b7eeded92ce7e Mon Sep 17 00:00:00 2001
From: Daniil Davidov <d.davydov@postgrespro.ru>
Date: Sun, 23 Nov 2025 01:08:14 +0700
Subject: [PATCH v20 4/5] Tests for parallel autovacuum
---
src/backend/commands/vacuumparallel.c | 60 +++
src/backend/postmaster/autovacuum.c | 19 +
src/include/postmaster/autovacuum.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_autovacuum/.gitignore | 2 +
src/test/modules/test_autovacuum/Makefile | 28 ++
src/test/modules/test_autovacuum/meson.build | 36 ++
.../modules/test_autovacuum/t/001_basic.pl | 346 ++++++++++++++++++
.../test_autovacuum/test_autovacuum--1.0.sql | 12 +
.../modules/test_autovacuum/test_autovacuum.c | 41 +++
.../test_autovacuum/test_autovacuum.control | 3 +
12 files changed, 550 insertions(+)
create mode 100644 src/test/modules/test_autovacuum/.gitignore
create mode 100644 src/test/modules/test_autovacuum/Makefile
create mode 100644 src/test/modules/test_autovacuum/meson.build
create mode 100644 src/test/modules/test_autovacuum/t/001_basic.pl
create mode 100644 src/test/modules/test_autovacuum/test_autovacuum--1.0.sql
create mode 100644 src/test/modules/test_autovacuum/test_autovacuum.c
create mode 100644 src/test/modules/test_autovacuum/test_autovacuum.control
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index e3561057334..bb4d2ef3bea 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -39,6 +39,7 @@
#include "postmaster/autovacuum.h"
#include "storage/bufmgr.h"
#include "tcop/tcopprot.h"
+#include "utils/injection_point.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -280,6 +281,7 @@ static void parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation
static bool parallel_vacuum_index_is_parallel_safe(Relation indrel, int num_index_scans,
bool vacuum);
static void parallel_vacuum_error_callback(void *arg);
+static void parallel_vacuum_report_cost_based_params(void);
/*
* Try to enter parallel mode and create a parallel context. Then initialize
@@ -887,6 +889,14 @@ parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scan
pvs->pcxt->nworkers_launched, nworkers)));
}
+ /*
+ * To be able to exercise whether all reserved parallel workers are being
+ * released anyway, allow injection points to trigger a failure at this
+ * point.
+ */
+ if (nworkers > 0)
+ INJECTION_POINT("autovacuum-leader-before-indexes-processing", NULL);
+
/* Vacuum the indexes that can be processed by only leader process */
parallel_vacuum_process_unsafe_indexes(pvs);
@@ -896,6 +906,15 @@ parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scan
*/
parallel_vacuum_process_safe_indexes(pvs);
+ /*
+ * To be able to exercise whether leader parallel autovacuum worker can
+ * propagate cost-based params to parallel workers, wait here until
+ * configuration is changed. I.e. tests are expecting, that during index
+ * processing vacuum_delay_point have been called (if config was changed).
+ */
+ if (nworkers > 0)
+ INJECTION_POINT("autovacuum-leader-after-indexes-processing", NULL);
+
/*
* Next, accumulate buffer and WAL usage. (This must wait for the workers
* to finish, or we might get incomplete data.)
@@ -1261,9 +1280,23 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
/* Prepare to track buffer usage during parallel execution */
InstrStartParallelQuery();
+ INJECTION_POINT("parallel-worker-before-indexes-processing", NULL);
+
/* Process indexes to perform vacuum/cleanup */
parallel_vacuum_process_safe_indexes(&pvs);
+#ifdef USE_INJECTION_POINTS
+ /*
+ * There is no guarantee that each parallel worker will necessarily
+ * process at least one index. Thus, at this point we cannot be sure that
+ * worker called vacuum_cost_delay. In order to test cost-based parameters
+ * propagation (from leader worker), call vacuum_delay_point here, if
+ * injection point is active.
+ */
+ if (IS_INJECTION_POINT_ATTACHED("parallel-autovacuum-force-delay-point"))
+ parallel_vacuum_report_cost_based_params();
+#endif
+
/* Report buffer/WAL usage during parallel execution */
buffer_usage = shm_toc_lookup(toc, PARALLEL_VACUUM_KEY_BUFFER_USAGE, false);
wal_usage = shm_toc_lookup(toc, PARALLEL_VACUUM_KEY_WAL_USAGE, false);
@@ -1316,3 +1349,30 @@ parallel_vacuum_error_callback(void *arg)
return;
}
}
+
+/*
+ * Log values of the related to cost-based delay parameters. It is used for
+ * testing purpose.
+ */
+static void
+parallel_vacuum_report_cost_based_params(void)
+{
+ StringInfoData buf;
+
+ /* Simulate config reload during normal processing */
+ pg_atomic_add_fetch_u32(VacuumActiveNWorkers, 1);
+ vacuum_delay_point(false);
+ pg_atomic_sub_fetch_u32(VacuumActiveNWorkers, 1);
+
+ initStringInfo(&buf);
+
+ appendStringInfo(&buf, "Vacuum cost-based delay parameters of parallel worker:\n");
+ appendStringInfo(&buf,"vacuum_cost_limit = %d\n",vacuum_cost_limit);
+ appendStringInfo(&buf, "vacuum_cost_delay = %g\n", vacuum_cost_delay);
+ appendStringInfo(&buf, "vacuum_cost_page_miss = %d\n", VacuumCostPageMiss);
+ appendStringInfo(&buf, "vacuum_cost_page_dirty = %d\n", VacuumCostPageDirty);
+ appendStringInfo(&buf, "vacuum_cost_page_hit = %d\n", VacuumCostPageHit);
+
+ ereport(LOG, errmsg("%s", buf.data));
+ pfree(buf.data);
+}
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 3ee858c5fbd..e6d60434bc5 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -801,6 +801,8 @@ ProcessAutoVacLauncherInterrupts(void)
/* rebuild the list in case the naptime changed */
rebuild_database_list(InvalidOid);
+
+ INJECTION_POINT("autovacuum-launcher-after-reload-config", NULL);
}
/* Process barrier events */
@@ -3467,6 +3469,23 @@ AutoVacuumReleaseAllParallelWorkers(void)
Assert(av_nworkers_reserved == 0);
}
+/*
+ * Get number of free autovacuum parallel workers.
+ *
+ * For testing purpose only!
+ */
+uint32
+AutoVacuumGetFreeParallelWorkers(void)
+{
+ uint32 nfree_workers;
+
+ LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);
+ nfree_workers = AutoVacuumShmem->av_freeParallelWorkers;
+ LWLockRelease(AutovacuumLock);
+
+ return nfree_workers;
+}
+
/*
* autovac_init
* This is called at postmaster initialization.
diff --git a/src/include/postmaster/autovacuum.h b/src/include/postmaster/autovacuum.h
index f3783afb51b..52be260e15f 100644
--- a/src/include/postmaster/autovacuum.h
+++ b/src/include/postmaster/autovacuum.h
@@ -66,6 +66,7 @@ extern bool AutoVacuumRequestWork(AutoVacuumWorkItemType type,
extern void AutoVacuumReserveParallelWorkers(int *nworkers);
extern void AutoVacuumReleaseParallelWorkers(int nworkers);
extern void AutoVacuumReleaseAllParallelWorkers(void);
+extern uint32 AutoVacuumGetFreeParallelWorkers(void);
/* shared memory stuff */
extern Size AutoVacuumShmemSize(void);
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4c6d56d97d8..bfe365fa575 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -16,6 +16,7 @@ SUBDIRS = \
plsample \
spgist_name_ops \
test_aio \
+ test_autovacuum \
test_binaryheap \
test_bitmapset \
test_bloomfilter \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 1b31c5b98d6..01a3e3ec044 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -16,6 +16,7 @@ subdir('plsample')
subdir('spgist_name_ops')
subdir('ssl_passphrase_callback')
subdir('test_aio')
+subdir('test_autovacuum')
subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
diff --git a/src/test/modules/test_autovacuum/.gitignore b/src/test/modules/test_autovacuum/.gitignore
new file mode 100644
index 00000000000..716e17f5a2a
--- /dev/null
+++ b/src/test/modules/test_autovacuum/.gitignore
@@ -0,0 +1,2 @@
+# Generated subdirectories
+/tmp_check/
diff --git a/src/test/modules/test_autovacuum/Makefile b/src/test/modules/test_autovacuum/Makefile
new file mode 100644
index 00000000000..32254c53a5d
--- /dev/null
+++ b/src/test/modules/test_autovacuum/Makefile
@@ -0,0 +1,28 @@
+# src/test/modules/test_autovacuum/Makefile
+
+PGFILEDESC = "test_autovacuum - test code for parallel autovacuum"
+
+MODULE_big = test_autovacuum
+OBJS = \
+ $(WIN32RES) \
+ test_autovacuum.o
+
+EXTENSION = test_autovacuum
+DATA = test_autovacuum--1.0.sql
+
+TAP_TESTS = 1
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_autovacuum
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
new file mode 100644
index 00000000000..3441e5e49cf
--- /dev/null
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -0,0 +1,36 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_autovacuum_sources = files(
+ 'test_autovacuum.c',
+)
+
+if host_system == 'windows'
+ test_autovacuum_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'test_autovacuum',
+ '--FILEDESC', 'test_autovacuum - test code for parallel autovacuum',])
+endif
+
+test_autovacuum = shared_module('test_autovacuum',
+ test_autovacuum_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_autovacuum
+
+test_install_data += files(
+ 'test_autovacuum.control',
+ 'test_autovacuum--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_autovacuum',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_autovacuum/t/001_basic.pl b/src/test/modules/test_autovacuum/t/001_basic.pl
new file mode 100644
index 00000000000..065a58ef2e6
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/001_basic.pl
@@ -0,0 +1,346 @@
+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';
+}
+
+# Before each test we should disable autovacuum for 'test_autovac' table and
+# generate some dead tuples in it.
+
+sub prepare_for_next_test
+{
+ my ($node, $test_number) = @_;
+
+ $node->safe_psql('postgres', qq{
+ ALTER TABLE test_autovac SET (autovacuum_enabled = false);
+ });
+
+ $node->safe_psql('postgres', qq{
+ UPDATE test_autovac SET col_1 = $test_number;
+ ANALYZE test_autovac;
+ });
+}
+
+sub wait_for_av_log
+{
+ my ($node, $expected_log) = @_;
+
+ $node->wait_for_log($expected_log);
+ truncate $node->logfile, 0 or die "truncate failed: $!";
+}
+
+my $psql_out;
+
+my $node = PostgreSQL::Test::Cluster->new('node1');
+$node->init;
+
+# Configure postgres, so it can launch parallel autovacuum workers, log all
+# information we are interested in and autovacuum works frequently
+$node->append_conf('postgresql.conf', qq{
+ max_worker_processes = 20
+ max_parallel_workers = 20
+ max_parallel_maintenance_workers = 20
+ autovacuum_max_parallel_workers = 20
+ log_min_messages = debug2
+ log_autovacuum_min_duration = 0
+ autovacuum_naptime = '1s'
+ min_parallel_index_scan_size = 0
+ shared_preload_libraries=test_autovacuum
+});
+$node->start;
+
+# Check if the extension injection_points is available, as it may be
+# possible that this script is run with installcheck, where the module
+# would not be installed by default.
+if (!$node->check_extension('injection_points'))
+{
+ plan skip_all => 'Extension injection_points not installed';
+}
+
+# Create all functions needed for testing
+$node->safe_psql('postgres', qq{
+ CREATE EXTENSION test_autovacuum;
+ CREATE EXTENSION injection_points;
+});
+
+my $indexes_num = 4;
+my $initial_rows_num = 10_000;
+my $autovacuum_parallel_workers = 2;
+
+# Create table with specified number of b-tree indexes on it
+$node->safe_psql('postgres', qq{
+ CREATE TABLE test_autovac (
+ id SERIAL PRIMARY KEY,
+ col_1 INTEGER, col_2 INTEGER, col_3 INTEGER, col_4 INTEGER
+ ) WITH (autovacuum_parallel_workers = $autovacuum_parallel_workers);
+
+ DO \$\$
+ DECLARE
+ i INTEGER;
+ BEGIN
+ FOR i IN 1..$indexes_num LOOP
+ EXECUTE format('CREATE INDEX idx_col_\%s ON test_autovac (col_\%s);', i, i);
+ END LOOP;
+ END \$\$;
+});
+
+# Insert specified tuples num into the table
+$node->safe_psql('postgres', qq{
+ DO \$\$
+ DECLARE
+ i INTEGER;
+ BEGIN
+ FOR i IN 1..$initial_rows_num LOOP
+ INSERT INTO test_autovac VALUES (i, i + 1, i + 2, i + 3);
+ END LOOP;
+ END \$\$;
+});
+
+# Test 1 :
+# Our table has enough indexes and appropriate reloptions, so autovacuum must
+# be able to process it in parallel mode. Just check if it can.
+# Also check whether all requested workers:
+# 1) launched
+# 2) correctly released
+
+prepare_for_next_test($node, 1);
+
+$node->safe_psql('postgres', qq{
+ ALTER TABLE test_autovac SET (autovacuum_enabled = true);
+});
+
+# Wait until the parallel autovacuum on table is completed. At the same time,
+# we check that the required number of parallel workers has been started.
+wait_for_av_log($node,
+ qr/parallel index vacuum\/cleanup: 2 workers were planned, / .
+ qr/2 workers were reserved and 2 workers were launched in total/);
+
+$node->psql('postgres',
+ "SELECT get_parallel_autovacuum_free_workers();",
+ stdout => \$psql_out,
+);
+is($psql_out, 20, 'All parallel workers has been released by the leader');
+
+# Test 2:
+# Check whether parallel autovacuum leader can propagate cost-based parameters
+# to parallel workers.
+
+prepare_for_next_test($node, 2);
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_attach('autovacuum-leader-before-indexes-processing', 'wait');
+ SELECT injection_points_attach('autovacuum-leader-after-indexes-processing', 'wait');
+ SELECT injection_points_attach('parallel-worker-before-indexes-processing', 'wait');
+ SELECT injection_points_attach('parallel-autovacuum-force-delay-point', 'wait');
+
+ ALTER TABLE test_autovac SET (autovacuum_parallel_workers = 1, autovacuum_enabled = true);
+});
+
+# Wait until parallel autovacuum leader launches parallel worker and falls
+# asleep on the injection point
+$node->wait_for_event(
+ 'autovacuum worker',
+ 'autovacuum-leader-before-indexes-processing'
+);
+
+# Reload config - leader worker must update its own parameters during indexes
+# processing
+$node->safe_psql('postgres', qq{
+ ALTER SYSTEM SET vacuum_cost_limit = 500;
+ ALTER SYSTEM SET vacuum_cost_page_miss = 10;
+ ALTER SYSTEM SET vacuum_cost_page_dirty = 10;
+ ALTER SYSTEM SET vacuum_cost_page_hit = 10;
+ SELECT pg_reload_conf();
+});
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_wakeup('autovacuum-leader-before-indexes-processing');
+});
+
+# Wait until leader worker is guaranteed to update parameters and propagate
+# their values to the parallel worker
+$node->wait_for_event(
+ 'autovacuum worker',
+ 'autovacuum-leader-after-indexes-processing'
+);
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_wakeup('autovacuum-leader-after-indexes-processing');
+});
+
+# Now wake up the parallel worker and force it to call vacuum_delay_point
+$node->wait_for_event(
+ 'parallel worker',
+ 'parallel-worker-before-indexes-processing'
+);
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_wakeup('parallel-worker-before-indexes-processing');
+});
+
+# Check whether worker successfully updated all parameters
+wait_for_av_log($node,
+ qr/Vacuum cost-based delay parameters of parallel worker:\n/ .
+ qr/\tvacuum_cost_limit = 500\n/ .
+ qr/\tvacuum_cost_delay = 2\n/ .
+ qr/\tvacuum_cost_page_miss = 10\n/ .
+ qr/\tvacuum_cost_page_dirty = 10\n/ .
+ qr/\tvacuum_cost_page_hit = 10\n/);
+
+# Cleanup
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_detach('autovacuum-leader-before-indexes-processing');
+ SELECT injection_points_detach('autovacuum-leader-after-indexes-processing');
+ SELECT injection_points_detach('parallel-worker-before-indexes-processing');
+ SELECT injection_points_detach('parallel-autovacuum-force-delay-point');
+
+ ALTER TABLE test_autovac SET (autovacuum_parallel_workers = $autovacuum_parallel_workers);
+});
+
+# Test 3:
+# Test adjustment of free parallel workers number when changing
+# autovacuum_max_parallel_workers parameter
+
+prepare_for_next_test($node, 4);
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_attach('autovacuum-leader-before-indexes-processing', 'wait');
+ SELECT injection_points_attach('autovacuum-launcher-after-reload-config', 'wait');
+ ALTER TABLE test_autovac SET (autovacuum_enabled = true);
+});
+
+$node->wait_for_event(
+ 'autovacuum worker',
+ 'autovacuum-leader-before-indexes-processing'
+);
+
+$node->safe_psql('postgres', qq{
+ ALTER SYSTEM SET autovacuum_max_parallel_workers = 1;
+ SELECT pg_reload_conf();
+});
+
+$node->wait_for_event(
+ 'autovacuum launcher',
+ 'autovacuum-launcher-after-reload-config'
+);
+
+# Since 2 parallel workers already launched and will be released in the future,
+# we are expecting that :
+# 1) number of free workers will be '0' after config reload
+# 2) number of free workers will be '1' after releasing workers
+
+# Check statement (1)
+$node->psql('postgres',
+ "SELECT get_parallel_autovacuum_free_workers();",
+ stdout => \$psql_out,
+);
+is($psql_out, 0,
+ 'Number of free parallel workers is consistent');
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_wakeup('autovacuum-launcher-after-reload-config');
+ SELECT injection_points_wakeup('autovacuum-leader-before-indexes-processing');
+});
+
+# Wait until the end of parallel processing
+wait_for_av_log($node,
+ qr/parallel index vacuum\/cleanup: 2 workers were planned, / .
+ qr/2 workers were reserved and 2 workers were launched in total/);
+
+# Check statement (2)
+$node->psql('postgres',
+ "SELECT get_parallel_autovacuum_free_workers();",
+ stdout => \$psql_out,
+);
+is($psql_out, 1,
+ 'Number of free parallel workers is consistent');
+
+# Cleanup
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_detach('autovacuum-leader-before-indexes-processing');
+ SELECT injection_points_detach('autovacuum-launcher-after-reload-config');
+ ALTER SYSTEM SET autovacuum_max_parallel_workers = 10;
+ SELECT pg_reload_conf();
+});
+
+# Test 4:
+# We want parallel autovacuum workers to be released even if leader gets an
+# error. At first, simulate situation, when leader exites due to an ERROR.
+
+prepare_for_next_test($node, 4);
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_attach('autovacuum-leader-before-indexes-processing', 'error');
+ ALTER TABLE test_autovac SET (autovacuum_enabled = true);
+});
+
+wait_for_av_log($node,
+ qr/error triggered for injection point / .
+ qr/autovacuum-leader-before-indexes-processing/);
+
+$node->psql('postgres',
+ "SELECT get_parallel_autovacuum_free_workers();",
+ stdout => \$psql_out,
+);
+is($psql_out, 10,
+ 'All parallel workers has been released by the leader after ERROR');
+
+# Cleanup
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_detach('autovacuum-leader-before-indexes-processing');
+});
+
+# Test 5:
+# Same as above test, but simulate situation, when leader exites due to FATAL.
+
+prepare_for_next_test($node, 5);
+
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_attach('autovacuum-leader-before-indexes-processing', 'wait');
+ ALTER TABLE test_autovac SET (autovacuum_enabled = true);
+});
+
+$node->wait_for_event(
+ 'autovacuum worker',
+ 'autovacuum-leader-before-indexes-processing'
+);
+
+my $av_pid = $node->safe_psql('postgres', qq{
+ SELECT pid FROM pg_stat_activity
+ WHERE backend_type = 'autovacuum worker'
+ AND wait_event = 'autovacuum-leader-before-indexes-processing'
+ LIMIT 1;
+});
+
+# Create role with pg_signal_autovacuum_worker for terminating autovacuum worker.
+$node->safe_psql('postgres', qq{
+ CREATE ROLE regress_worker_role;
+ GRANT pg_signal_autovacuum_worker TO regress_worker_role;
+ SET ROLE regress_worker_role;
+});
+
+$node->safe_psql('postgres', qq{
+ SELECT pg_terminate_backend('$av_pid');
+});
+
+wait_for_av_log($node,
+ qr/terminating autovacuum process due to administrator command/);
+
+$node->psql('postgres',
+ "SELECT get_parallel_autovacuum_free_workers();",
+ stdout => \$psql_out,
+);
+is($psql_out, 10,
+ 'All parallel workers has been released by the leader after FATAL');
+
+# Cleanup
+$node->safe_psql('postgres', qq{
+ SELECT injection_points_detach('autovacuum-leader-before-indexes-processing');
+});
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_autovacuum/test_autovacuum--1.0.sql b/src/test/modules/test_autovacuum/test_autovacuum--1.0.sql
new file mode 100644
index 00000000000..e5646e0def5
--- /dev/null
+++ b/src/test/modules/test_autovacuum/test_autovacuum--1.0.sql
@@ -0,0 +1,12 @@
+/* src/test/modules/test_autovacuum/test_autovacuum--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_autovacuum" to load this file. \quit
+
+/*
+ * Functions for expecting shared autovacuum state
+ */
+
+CREATE FUNCTION get_parallel_autovacuum_free_workers()
+RETURNS INTEGER STRICT
+AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_autovacuum/test_autovacuum.c b/src/test/modules/test_autovacuum/test_autovacuum.c
new file mode 100644
index 00000000000..959629c7685
--- /dev/null
+++ b/src/test/modules/test_autovacuum/test_autovacuum.c
@@ -0,0 +1,41 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_autovacuum.c
+ * Helpers to write tests for parallel autovacuum
+ *
+ * Copyright (c) 2020-2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_autovacuum/test_autovacuum.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "commands/vacuum.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/autovacuum.h"
+#include "storage/shmem.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/builtins.h"
+#include "utils/injection_point.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(get_parallel_autovacuum_free_workers);
+Datum
+get_parallel_autovacuum_free_workers(PG_FUNCTION_ARGS)
+{
+ uint32 nfree_workers;
+
+#ifndef USE_INJECTION_POINTS
+ ereport(ERROR, errmsg("injection points not supported"));
+#endif
+
+ nfree_workers = AutoVacuumGetFreeParallelWorkers();
+
+ PG_RETURN_UINT32(nfree_workers);
+}
diff --git a/src/test/modules/test_autovacuum/test_autovacuum.control b/src/test/modules/test_autovacuum/test_autovacuum.control
new file mode 100644
index 00000000000..1b7fad258f0
--- /dev/null
+++ b/src/test/modules/test_autovacuum/test_autovacuum.control
@@ -0,0 +1,3 @@
+comment = 'Test code for parallel autovacuum'
+default_version = '1.0'
+module_pathname = '$libdir/test_autovacuum'
--
2.43.0