From: Masahiko Sawada Date: Mon, 6 Apr 2026 18:48:29 +0000 (-0700) Subject: Allow autovacuum to use parallel vacuum workers. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1ff3180ca0169556984ab83759477f593129794d;p=thirdparty%2Fpostgresql.git Allow autovacuum to use parallel vacuum workers. Previously, autovacuum always disabled parallel vacuum regardless of the table's index count or configuration. This commit enables autovacuum workers to use parallel index vacuuming and index cleanup, using the same parallel vacuum infrastructure as manual VACUUM. Two new configuration options control the feature. The GUC autovacuum_max_parallel_workers sets the maximum number of parallel workers a single autovacuum worker may launch; it defaults to 0, preserving existing behavior unless explicitly enabled. The per-table storage parameter autovacuum_parallel_workers provides per-table limits. A value of 0 disables parallel vacuum for the table, a positive value caps the worker count (still bounded by the GUC), and -1 (the default) defers to the GUC. To handle cases where autovacuum workers receive a SIGHUP and update their cost-based vacuum delay parameters mid-operation, a new propagation mechanism is added to vacuumparallel.c. The leader stores its effective cost parameters in a DSM segment. Parallel vacuum workers poll for changes in vacuum_delay_point(); if an update is detected, they apply the new values locally via VacuumUpdateCosts(). A new test module, src/test/modules/test_autovacuum, is added to verify that parallel autovacuum workers are correctly launched and that cost-parameter updates are propagated as expected. The patch was originally proposed by Maxim Orlov, but the implementation has undergone significant architectural changes since then during the review process. Author: Daniil Davydov <3danissimo@gmail.com> Reviewed-by: Masahiko Sawada Reviewed-by: Sami Imseih Reviewed-by: Matheus Alcantara Reviewed-by: Bharath Rupireddy Reviewed-by: Alexander Korotkov Reviewed-by: zengman Discussion: https://postgr.es/m/CACG=ezZOrNsuLoETLD1gAswZMuH2nGGq7Ogcc0QOE5hhWaw=cw@mail.gmail.com --- diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index b44231a362d..3324d2d3c49 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -2918,6 +2918,7 @@ include_dir 'conf.d' When changing this value, consider also adjusting , + , , and . @@ -9528,6 +9529,29 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; + + autovacuum_max_parallel_workers (integer) + + autovacuum_max_parallel_workers + configuration parameter + + + + + Sets the maximum number of parallel workers that can be used by a + single autovacuum worker to process indexes. This limit applies + specifically to the index vacuuming and index cleanup phases (for the + details of each autovacuum phase, please refer to ). + The actual number of parallel workers is further limited by + . This is the + per-autovacuum worker equivalent of the PARALLEL + option of the VACUUM + command. Setting this value to 0 disables parallel vacuum during autovacuum. + The default is 0. + + + + diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 0d2a28207ed..64bbc831343 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -1038,6 +1038,10 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu per-table autovacuum_vacuum_cost_delay or autovacuum_vacuum_cost_limit storage parameters have been set are not considered in the balancing algorithm. + Parallel workers launched for are using + the same cost delay parameters as the leader worker. If any of these + parameters are changed in the leader worker, it will propagate the new + parameter values to all of its parallel workers. @@ -1166,6 +1170,36 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu + + + Parallel Vacuum + + + VACUUM can perform index vacuuming and index cleanup + phases in parallel using background workers (for the details of each + vacuum phase, please refer to ). The + degree of parallelism is determined by the number of indexes on the + relation that support parallel vacuum. For manual VACUUM, + this is limited by the PARALLEL option, which is + further capped by . + For autovacuum, it is limited by the table's + if any which is + capped limited by + parameter. Please + note that it is not guaranteed that the number of parallel workers that was + calculated will be used during execution. It is possible for a vacuum to + run with fewer workers than specified, or even with no workers at all. + + + + An index can participate in parallel vacuum if and only if the size of the + index is more than . + Only one worker can be used per index. So parallel workers are launched + only when there are at least 2 indexes in the table. + Workers for vacuum are launched before the start of each phase and exit at + the end of the phase. These behaviors might change in a future release. + + diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 80829b23945..e342585c7f0 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -1738,6 +1738,22 @@ WITH ( MODULUS numeric_literal, REM + + autovacuum_parallel_workers (integer) + + autovacuum_parallel_workers storage parameter + + + + + Per-table value for + parameter. If -1 is specified, autovacuum_max_parallel_workers + value will be used. If set to 0, parallel vacuum is disabled for + this table. The default value is -1. + + + + autovacuum_vacuum_threshold, toast.autovacuum_vacuum_threshold (integer) diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index ac5d083d468..38ee973ea05 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -81,7 +81,7 @@ VACUUM [ ( option [, ...] ) ] [ parallel vacuum. + indexes. This feature is known as . To disable this feature, one can use PARALLEL option and specify parallel workers as zero. VACUUM FULL rewrites the entire contents of the table into a new disk file with no extra space, @@ -266,24 +266,9 @@ VACUUM [ ( option [, ...] ) ] [ PARALLEL - Perform index vacuum and index cleanup phases of VACUUM - in parallel using integer - background workers (for the details of each vacuum phase, please - refer to ). The number of workers used - to perform the operation is equal to the number of indexes on the - relation that support parallel vacuum which is limited by the number of - workers specified with PARALLEL option if any which is - further limited by . - An index can participate in parallel vacuum if and only if the size of the - index is more than . - Please note that it is not guaranteed that the number of parallel workers - specified in integer will be - used during execution. It is possible for a vacuum to run with fewer - workers than specified, or even with no workers at all. Only one worker - can be used per index. So parallel workers are launched only when there - are at least 2 indexes in the table. Workers for - vacuum are launched before the start of each phase and exit at the end of - the phase. These behaviors might change in a future release. This + Specifies the maximum number of parallel workers that can be used + for , which is further limited + by . This option can't be used with the FULL option. diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index b41eafd7691..3e832c3797e 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -236,6 +236,15 @@ static relopt_int intRelOpts[] = }, SPGIST_DEFAULT_FILLFACTOR, SPGIST_MIN_FILLFACTOR, 100 }, + { + { + "autovacuum_parallel_workers", + "Maximum number of parallel autovacuum workers that can be used for processing this table.", + RELOPT_KIND_HEAP, + ShareUpdateExclusiveLock + }, + -1, -1, 1024 + }, { { "autovacuum_vacuum_threshold", @@ -1969,6 +1978,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)}, {"autovacuum_enabled", RELOPT_TYPE_BOOL, offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)}, + {"autovacuum_parallel_workers", RELOPT_TYPE_INT, + offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)}, {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT, offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)}, {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT, diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 88c71cd85b6..39395aed0d5 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -152,6 +152,7 @@ #include "storage/latch.h" #include "storage/lmgr.h" #include "storage/read_stream.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/pg_rusage.h" #include "utils/timestamp.h" @@ -862,6 +863,17 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, lazy_check_wraparound_failsafe(vacrel); dead_items_alloc(vacrel, params->nworkers); +#ifdef USE_INJECTION_POINTS + + /* + * Used by tests to pause before parallel vacuum is launched, allowing + * test code to modify configuration that the leader then propagates to + * workers. + */ + if (AmAutoVacuumWorkerProcess() && ParallelVacuumIsActive(vacrel)) + INJECTION_POINT("autovacuum-start-parallel-vacuum", NULL); +#endif + /* * Call lazy_scan_heap to perform all required heap pruning, index * vacuuming, and heap vacuuming (plus related processing) diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index b179b62b5c8..a67a70df297 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -2435,8 +2435,20 @@ vacuum_delay_point(bool is_analyze) /* Always check for interrupts */ CHECK_FOR_INTERRUPTS(); - if (InterruptPending || - (!VacuumCostActive && !ConfigReloadPending)) + if (InterruptPending) + return; + + if (IsParallelWorker()) + { + /* + * Update cost-based vacuum delay parameters for a parallel autovacuum + * worker if any changes are detected. It might enable cost-based + * delay so it needs to be called before VacuumCostActive check. + */ + parallel_vacuum_update_shared_delay_params(); + } + + if (!VacuumCostActive && !ConfigReloadPending) return; /* @@ -2450,6 +2462,12 @@ vacuum_delay_point(bool is_analyze) ConfigReloadPending = false; ProcessConfigFile(PGC_SIGHUP); VacuumUpdateCosts(); + + /* + * Propagate cost-based vacuum delay parameters to shared memory if + * any of them have changed during the config reload. + */ + parallel_vacuum_propagate_shared_delay_params(); } /* diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index 77834b96a21..979c2be4abd 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -1,7 +1,9 @@ /*------------------------------------------------------------------------- * * vacuumparallel.c - * Support routines for parallel vacuum execution. + * Support routines for parallel vacuum and autovacuum execution. In the + * comments below, the word "vacuum" will refer to both vacuum and + * autovacuum. * * This file contains routines that are intended to support setting up, using, * and tearing down a ParallelVacuumState. @@ -16,6 +18,13 @@ * the parallel context is re-initialized so that the same DSM can be used for * multiple passes of index bulk-deletion and index cleanup. * + * For parallel autovacuum, we need to propagate cost-based vacuum delay + * parameters from the leader to its workers, as the leader's parameters can + * change even while processing a table (e.g., due to a config reload). + * The PVSharedCostParams struct manages these parameters using a + * generation counter. Each parallel worker polls this shared state and + * refreshes its local delay parameters whenever a change is detected. + * * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * @@ -51,6 +60,33 @@ #define PARALLEL_VACUUM_KEY_WAL_USAGE 4 #define PARALLEL_VACUUM_KEY_INDEX_STATS 5 +/* + * Struct for cost-based vacuum delay related parameters to share among an + * autovacuum worker and its parallel vacuum workers. + */ +typedef struct PVSharedCostParams +{ + /* + * The generation counter is incremented by the leader process each time + * it updates the shared cost-based vacuum delay parameters. Parallel + * vacuum workers compare it with their local generation, + * shared_params_generation_local, to detect whether they need to refresh + * their local parameters. The generation starts from 1 so that a freshly + * started worker (whose local copy is 0) will always load the initial + * parameters on its first check. + */ + pg_atomic_uint32 generation; + + slock_t mutex; /* protects all fields below */ + + /* Parameters to share with parallel workers */ + double cost_delay; + int cost_limit; + int cost_page_dirty; + int cost_page_hit; + int cost_page_miss; +} PVSharedCostParams; + /* * Shared information among parallel workers. So this is allocated in the DSM * segment. @@ -120,6 +156,18 @@ typedef struct PVShared /* Statistics of shared dead items */ VacDeadItemsInfo dead_items_info; + + /* + * If 'true' then we are running parallel autovacuum. Otherwise, we are + * running parallel maintenance VACUUM. + */ + bool is_autovacuum; + + /* + * Cost-based vacuum delay parameters shared between the autovacuum leader + * and its parallel workers. + */ + PVSharedCostParams cost_params; } PVShared; /* Status used during parallel index vacuum or cleanup */ @@ -222,6 +270,17 @@ struct ParallelVacuumState PVIndVacStatus status; }; +static PVSharedCostParams *pv_shared_cost_params = NULL; + +/* + * Worker-local copy of the last cost-parameter generation this worker has + * applied. Initialized to 0; since the leader initializes the shared + * generation counter to 1, the first call to + * parallel_vacuum_update_shared_delay_params() will always detect a + * mismatch and read the initial parameters from shared memory. + */ +static uint32 shared_params_generation_local = 0; + static int parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int nrequested, bool *will_parallel_vacuum); static void parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scans, @@ -233,6 +292,8 @@ 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 inline void parallel_vacuum_set_cost_parameters(PVSharedCostParams *params); +static void parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg); /* * Try to enter parallel mode and create a parallel context. Then initialize @@ -374,8 +435,9 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes, shared->queryid = pgstat_get_my_query_id(); shared->maintenance_work_mem_worker = (nindexes_mwm > 0) ? - maintenance_work_mem / Min(parallel_workers, nindexes_mwm) : - maintenance_work_mem; + vac_work_mem / Min(parallel_workers, nindexes_mwm) : + vac_work_mem; + shared->dead_items_info.max_bytes = vac_work_mem * (size_t) 1024; /* Prepare DSA space for dead items */ @@ -392,6 +454,22 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes, pg_atomic_init_u32(&(shared->active_nworkers), 0); pg_atomic_init_u32(&(shared->idx), 0); + shared->is_autovacuum = AmAutoVacuumWorkerProcess(); + + /* + * Initialize shared cost-based vacuum delay parameters if it's for + * autovacuum. + */ + if (shared->is_autovacuum) + { + parallel_vacuum_set_cost_parameters(&shared->cost_params); + pg_atomic_init_u32(&shared->cost_params.generation, 1); + SpinLockInit(&shared->cost_params.mutex); + + pv_shared_cost_params = &(shared->cost_params); + on_dsm_detach(pcxt->seg, parallel_vacuum_dsm_detach, (Datum) 0); + } + shm_toc_insert(pcxt->toc, PARALLEL_VACUUM_KEY_SHARED, shared); pvs->shared = shared; @@ -457,10 +535,26 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats) DestroyParallelContext(pvs->pcxt); ExitParallelMode(); + if (AmAutoVacuumWorkerProcess()) + pv_shared_cost_params = NULL; + pfree(pvs->will_parallel_vacuum); pfree(pvs); } +/* + * DSM detach callback. This is invoked when an autovacuum worker detaches + * from the DSM segment holding PVShared. It ensures to reset the local pointer + * to the shared state even if paralell vacuum raises an error and doesn't + * call parallel_vacuum_end(). + */ +static void +parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg) +{ + Assert(AmAutoVacuumWorkerProcess()); + pv_shared_cost_params = NULL; +} + /* * Returns the dead items space and dead items information. */ @@ -534,6 +628,103 @@ parallel_vacuum_cleanup_all_indexes(ParallelVacuumState *pvs, long num_table_tup parallel_vacuum_process_all_indexes(pvs, num_index_scans, false, wstats); } +/* + * Fill in the given structure with cost-based vacuum delay parameter values. + */ +static inline void +parallel_vacuum_set_cost_parameters(PVSharedCostParams *params) +{ + params->cost_delay = vacuum_cost_delay; + params->cost_limit = vacuum_cost_limit; + params->cost_page_dirty = VacuumCostPageDirty; + params->cost_page_hit = VacuumCostPageHit; + params->cost_page_miss = VacuumCostPageMiss; +} + +/* + * Updates the cost-based vacuum delay parameters for parallel autovacuum + * workers. + * + * For non-autovacuum parallel workers, this function will have no effect. + */ +void +parallel_vacuum_update_shared_delay_params(void) +{ + uint32 params_generation; + + Assert(IsParallelWorker()); + + /* Quick return if the worker is not running for the autovacuum */ + if (pv_shared_cost_params == NULL) + return; + + params_generation = pg_atomic_read_u32(&pv_shared_cost_params->generation); + Assert(shared_params_generation_local <= params_generation); + + /* Return if parameters had not changed in the leader */ + if (params_generation == shared_params_generation_local) + return; + + SpinLockAcquire(&pv_shared_cost_params->mutex); + VacuumCostDelay = pv_shared_cost_params->cost_delay; + VacuumCostLimit = pv_shared_cost_params->cost_limit; + VacuumCostPageDirty = pv_shared_cost_params->cost_page_dirty; + VacuumCostPageHit = pv_shared_cost_params->cost_page_hit; + VacuumCostPageMiss = pv_shared_cost_params->cost_page_miss; + SpinLockRelease(&pv_shared_cost_params->mutex); + + VacuumUpdateCosts(); + + shared_params_generation_local = params_generation; + + elog(DEBUG2, + "parallel autovacuum worker updated cost params: cost_limit=%d, cost_delay=%g, cost_page_miss=%d, cost_page_dirty=%d, cost_page_hit=%d", + vacuum_cost_limit, + vacuum_cost_delay, + VacuumCostPageMiss, + VacuumCostPageDirty, + VacuumCostPageHit); +} + +/* + * Store the cost-based vacuum delay parameters in the shared memory so that + * parallel vacuum workers can consume them (see + * parallel_vacuum_update_shared_delay_params()). + */ +void +parallel_vacuum_propagate_shared_delay_params(void) +{ + Assert(AmAutoVacuumWorkerProcess()); + + /* + * Quick return if the leader process is not sharing the delay parameters. + */ + if (pv_shared_cost_params == NULL) + return; + + /* + * Check if any delay parameters have changed. We can read them without + * locks as only the leader can modify them. + */ + if (vacuum_cost_delay == pv_shared_cost_params->cost_delay && + vacuum_cost_limit == pv_shared_cost_params->cost_limit && + VacuumCostPageDirty == pv_shared_cost_params->cost_page_dirty && + VacuumCostPageHit == pv_shared_cost_params->cost_page_hit && + VacuumCostPageMiss == pv_shared_cost_params->cost_page_miss) + return; + + /* Update the shared delay parameters */ + SpinLockAcquire(&pv_shared_cost_params->mutex); + parallel_vacuum_set_cost_parameters(pv_shared_cost_params); + SpinLockRelease(&pv_shared_cost_params->mutex); + + /* + * Increment the generation of the parameters, i.e. let parallel workers + * know that they should re-read shared cost params. + */ + pg_atomic_fetch_add_u32(&pv_shared_cost_params->generation, 1); +} + /* * Compute the number of parallel worker processes to request. Both index * vacuum and index cleanup can be executed with parallel workers. @@ -555,12 +746,17 @@ parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int nrequested, int nindexes_parallel_bulkdel = 0; int nindexes_parallel_cleanup = 0; int parallel_workers; + int max_workers; + + max_workers = AmAutoVacuumWorkerProcess() ? + autovacuum_max_parallel_workers : + max_parallel_maintenance_workers; /* * We don't allow performing parallel operation in standalone backend or * when parallelism is disabled. */ - if (!IsUnderPostmaster || max_parallel_maintenance_workers == 0) + if (!IsUnderPostmaster || max_workers == 0) return 0; /* @@ -599,8 +795,8 @@ parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int nrequested, parallel_workers = (nrequested > 0) ? Min(nrequested, nindexes_parallel) : nindexes_parallel; - /* Cap by max_parallel_maintenance_workers */ - parallel_workers = Min(parallel_workers, max_parallel_maintenance_workers); + /* Cap by GUC variable */ + parallel_workers = Min(parallel_workers, max_workers); return parallel_workers; } @@ -1064,7 +1260,21 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) shared->dead_items_handle); /* Set cost-based vacuum delay */ - VacuumUpdateCosts(); + if (shared->is_autovacuum) + { + /* + * Parallel autovacuum workers initialize cost-based delay parameters + * from the leader's shared state rather than GUC defaults, because + * the leader may have applied per-table or autovacuum-specific + * overrides. pv_shared_cost_params must be set before calling + * parallel_vacuum_update_shared_delay_params(). + */ + pv_shared_cost_params = &(shared->cost_params); + parallel_vacuum_update_shared_delay_params(); + } + else + VacuumUpdateCosts(); + VacuumCostBalance = 0; VacuumCostBalanceLocal = 0; VacuumSharedCostBalance = &(shared->cost_balance); @@ -1119,6 +1329,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) vac_close_indexes(nindexes, indrels, RowExclusiveLock); table_close(rel, ShareUpdateExclusiveLock); FreeAccessStrategy(pvs.bstrategy); + + if (shared->is_autovacuum) + pv_shared_cost_params = NULL; } /* diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index c4d6b8811bf..5c63820383a 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -1698,7 +1698,7 @@ VacuumUpdateCosts(void) } else { - /* Must be explicit VACUUM or ANALYZE */ + /* Must be explicit VACUUM or ANALYZE or parallel autovacuum worker */ vacuum_cost_delay = VacuumCostDelay; vacuum_cost_limit = VacuumCostLimit; } @@ -2940,8 +2940,6 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, */ tab->at_params.index_cleanup = VACOPTVALUE_UNSPECIFIED; tab->at_params.truncate = VACOPTVALUE_UNSPECIFIED; - /* As of now, we don't support parallel vacuum for autovacuum */ - tab->at_params.nworkers = -1; tab->at_params.freeze_min_age = freeze_min_age; tab->at_params.freeze_table_age = freeze_table_age; tab->at_params.multixact_freeze_min_age = multixact_freeze_min_age; @@ -2951,6 +2949,27 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, tab->at_params.log_analyze_min_duration = log_analyze_min_duration; tab->at_params.toast_parent = InvalidOid; + /* Determine the number of parallel vacuum workers to use */ + tab->at_params.nworkers = 0; + if (avopts) + { + if (avopts->autovacuum_parallel_workers == 0) + { + /* + * Disable parallel vacuum, if the reloption sets the parallel + * degree as zero. + */ + tab->at_params.nworkers = -1; + } + else if (avopts->autovacuum_parallel_workers > 0) + tab->at_params.nworkers = avopts->autovacuum_parallel_workers; + + /* + * autovacuum_parallel_workers == -1 falls through, keep + * nworkers=0 + */ + } + /* * Later, in vacuum_rel(), we check reloptions for any * vacuum_max_eager_freeze_failure_rate override. diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..24ddb276f0c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -143,6 +143,7 @@ int NBuffers = 16384; int MaxConnections = 100; int max_worker_processes = 8; int max_parallel_workers = 8; +int autovacuum_max_parallel_workers = 0; int MaxBackends = 0; /* GUC parameters for vacuum */ diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index e1546d9c97a..c4c3fbc4fe3 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -3358,9 +3358,15 @@ set_config_with_handle(const char *name, config_handle *handle, * * Also allow normal setting if the GUC is marked GUC_ALLOW_IN_PARALLEL. * - * Other changes might need to affect other workers, so forbid them. + * Other changes might need to affect other workers, so forbid them. Note, + * that parallel autovacuum leader is an exception because cost-based + * delays need to be affected to parallel autovacuum workers. These + * parameters are propagated to its workers during parallel vacuum (see + * vacuumparallel.c for details). All other changes will affect only the + * parallel autovacuum leader. */ - if (IsInParallelMode() && changeVal && action != GUC_ACTION_SAVE && + if (IsInParallelMode() && !AmAutoVacuumWorkerProcess() && changeVal && + action != GUC_ACTION_SAVE && (record->flags & GUC_ALLOW_IN_PARALLEL) == 0) { ereport(elevel, diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 7a8a5d0764c..fcb6ab80583 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -170,6 +170,14 @@ max => '10.0', }, +{ name => 'autovacuum_max_parallel_workers', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Maximum number of parallel workers that can be used by a single autovacuum worker.', + variable => 'autovacuum_max_parallel_workers', + boot_val => '0', + min => '0', + max => 'MAX_PARALLEL_WORKER_LIMIT', +}, + { name => 'autovacuum_max_workers', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', short_desc => 'Sets the maximum number of simultaneously running autovacuum worker processes.', variable => 'autovacuum_max_workers', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 10a281dfd4b..e3e462f3efb 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -714,6 +714,7 @@ #autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # (change requires restart) #autovacuum_max_workers = 3 # max number of autovacuum subprocesses +#autovacuum_max_parallel_workers = 0 # limited by max_parallel_workers #autovacuum_naptime = 1min # time between autovacuum runs #autovacuum_vacuum_threshold = 50 # min number of row updates before # vacuum diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 53bf1e21721..1d941c11997 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1432,6 +1432,7 @@ static const char *const table_storage_parameters[] = { "autovacuum_multixact_freeze_max_age", "autovacuum_multixact_freeze_min_age", "autovacuum_multixact_freeze_table_age", + "autovacuum_parallel_workers", "autovacuum_vacuum_cost_delay", "autovacuum_vacuum_cost_limit", "autovacuum_vacuum_insert_scale_factor", diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index 5b8023616c0..69fec07491b 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -422,6 +422,8 @@ extern void parallel_vacuum_cleanup_all_indexes(ParallelVacuumState *pvs, int num_index_scans, bool estimated_count, PVWorkerStats *wstats); +extern void parallel_vacuum_update_shared_delay_params(void); +extern void parallel_vacuum_propagate_shared_delay_params(void); extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc); /* in commands/analyze.c */ diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 7277c37e779..2e10e3c814d 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -178,6 +178,7 @@ extern PGDLLIMPORT int MaxBackends; extern PGDLLIMPORT int MaxConnections; extern PGDLLIMPORT int max_worker_processes; extern PGDLLIMPORT int max_parallel_workers; +extern PGDLLIMPORT int autovacuum_max_parallel_workers; extern PGDLLIMPORT int commit_timestamp_buffers; extern PGDLLIMPORT int multixact_member_buffers; diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 236830f6b93..cd1e92f2302 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -311,6 +311,8 @@ typedef struct ForeignKeyCacheInfo typedef struct AutoVacOpts { bool enabled; + + int autovacuum_parallel_workers; int vacuum_threshold; int vacuum_max_threshold; int vacuum_ins_threshold; diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index f1b04c99969..0a74ab5c86f 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 fc99552d9ab..4bca42bb370 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..15e83010c1c --- /dev/null +++ b/src/test/modules/test_autovacuum/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/test_autovacuum/Makefile + +PGFILEDESC = "test_autovacuum - test code for autovacuum" + +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..86e392bc0de --- /dev/null +++ b/src/test/modules/test_autovacuum/meson.build @@ -0,0 +1,15 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +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_parallel_autovacuum.pl', + ], + }, +} diff --git a/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl new file mode 100644 index 00000000000..c5a2e78246a --- /dev/null +++ b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl @@ -0,0 +1,189 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test parallel autovacuum behavior + +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'; +} + +# Before each test we should disable autovacuum for 'test_autovac' table and +# generate some dead tuples in it. Returns the current autovacuum_count of +# the table test_autovac. +sub prepare_for_next_test +{ + my ($node, $test_number) = @_; + + $node->safe_psql( + 'postgres', qq{ + ALTER TABLE test_autovac SET (autovacuum_enabled = false); + UPDATE test_autovac SET col_1 = $test_number; + }); + + my $count = $node->safe_psql( + 'postgres', qq{ + SELECT autovacuum_count FROM pg_stat_user_tables WHERE relname = 'test_autovac' + }); + + return $count; +} + +# Wait for the table to be vacuumed by an autovacuum worker. +sub wait_for_autovacuum_complete +{ + my ($node, $old_count) = @_; + + $node->poll_query_until( + 'postgres', qq{ + SELECT autovacuum_count > $old_count FROM pg_stat_user_tables WHERE relname = 'test_autovac' + }); +} + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; + +# Limit to one autovacuum worker and disable autovacuum logging globally +# (enabled only on the test table) so that log checks below match only +# activity on the expected table. +$node->append_conf( + 'postgresql.conf', qq{ +autovacuum_max_workers = 1 +autovacuum_worker_slots = 1 +autovacuum_max_parallel_workers = 2 +max_worker_processes = 10 +max_parallel_workers = 10 +log_min_messages = debug2 +autovacuum_naptime = '1s' +min_parallel_index_scan_size = 0 +log_autovacuum_min_duration = -1 +}); +$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 injection_points; +}); + +my $indexes_num = 3; +my $initial_rows_num = 10_000; +my $autovacuum_parallel_workers = 2; + +# Create table and fill it with some data +$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, + log_autovacuum_min_duration = 0); + + INSERT INTO test_autovac + SELECT + g AS col1, + g + 1 AS col2, + g + 2 AS col3, + g + 3 AS col4 + FROM generate_series(1, $initial_rows_num) AS g; +}); + +# Create specified number of b-tree indexes on the table +$node->safe_psql( + 'postgres', qq{ + 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 \$\$; +}); + +# 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 do it. + +my $av_count = prepare_for_next_test($node, 1); +my $log_offset = -s $node->logfile; + +$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_autovacuum_complete($node, $av_count); +ok( $node->log_contains( + qr/parallel workers: index vacuum: 2 planned, 2 launched in total/, + $log_offset)); + +# Test 2: +# Check whether parallel autovacuum leader can propagate cost-based parameters +# to the parallel workers. + +$av_count = prepare_for_next_test($node, 2); +$log_offset = -s $node->logfile; + +$node->safe_psql( + 'postgres', qq{ + SELECT injection_points_attach('autovacuum-start-parallel-vacuum', 'wait'); + + ALTER TABLE test_autovac SET (autovacuum_parallel_workers = 1, autovacuum_enabled = true); +}); + +# Wait until parallel autovacuum is inited +$node->wait_for_event('autovacuum worker', + 'autovacuum-start-parallel-vacuum'); + +# Update the shared cost-based delay parameters. +$node->safe_psql( + 'postgres', qq{ + ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 500; + ALTER SYSTEM SET autovacuum_vacuum_cost_delay = 5; + 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(); +}); + +# Resume the leader process to update the shared parameters during heap scan (i.e. +# vacuum_delay_point() is called) and launch a parallel vacuum worker, but it stops +# before vacuuming indexes due to the injection point. +$node->safe_psql( + 'postgres', qq{ + SELECT injection_points_wakeup('autovacuum-start-parallel-vacuum'); +}); + +# Check whether parallel worker successfully updated all parameters during +# index processing. +$node->wait_for_log( + qr/parallel autovacuum worker updated cost params: cost_limit=500, cost_delay=5, cost_page_miss=10, cost_page_dirty=10, cost_page_hit=10/, + $log_offset); + +wait_for_autovacuum_complete($node, $av_count); + +# Cleanup +$node->safe_psql( + 'postgres', qq{ + SELECT injection_points_detach('autovacuum-start-parallel-vacuum'); +}); + +$node->stop; +done_testing(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e9430e07b36..4f8cc27d5b8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2112,6 +2112,7 @@ PVIndStats PVIndVacStatus PVOID PVShared +PVSharedCostParams PVWorkerUsage PVWorkerStats PX_Alias