From 59b4c5cf2bb391a8d20f8eb37fde3b32d2570eb4 Mon Sep 17 00:00:00 2001 From: Ewan Young Date: Tue, 2 Jun 2026 00:18:20 +0800 Subject: [PATCH v1] Don't extend the visibility map fork during non-read-only on-access pruning On-access pruning (heap_page_prune_opt) pins the visibility map page of the scanned relation, and visibilitymap_pin() extends the VM fork when it does not exist yet. Since 378a216187a made INSERT set pd_prune_xid, on-access pruning now fires on insert-mostly catalogs such as pg_database. The autovacuum launcher scans pg_database in get_database_list() using a catalog scan (rel_read_only = false), so when it reaches a full, prunable pg_database page whose VM fork does not exist, it tries to extend that fork. The launcher, like the bgwriter and checkpointer, is not allowed to perform IOOP_EXTEND, so pgstat_count_io_op() trips the pgstat_tracks_io_op() assertion and the launcher aborts. Only read-only scans actually set the VM during on-access pruning, and those run only in regular backends, which are permitted to extend relations. So extend the VM fork only on read-only scans; for every other scan (including the launcher's catalog scan) pin an already-existing VM page without extending, using visibilitymap_get_status(). VM corruption detection still works -- it goes through visibilitymap_get_status(), which never extends -- we simply decline to create the fork on access and leave that to the next VACUUM. prune_freeze_setup() can now legitimately receive an invalid vmbuffer for PRUNE_ON_ACCESS (when the fork is absent), so relax its assertion. --- src/backend/access/heap/pruneheap.c | 32 ++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index fdddd23035b..0189ee7a6ea 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -336,8 +336,26 @@ heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer, PruneFreezeResult presult; PruneFreezeParams params; - visibilitymap_pin(relation, BufferGetBlockNumber(buffer), - vmbuffer); + /* + * Pin the visibility map page covering this heap block. If this + * scan may set the VM (the relation is read-only for this query), + * pin for update, extending the VM fork if necessary. Otherwise we + * only need an already-existing page for VM corruption checking, so + * avoid creating the fork: it is both wasteful and, for process + * types that must never extend a relation, fatal. In particular + * the autovacuum launcher prunes pg_database on-access while + * scanning it in get_database_list(), and it is forbidden from + * performing IOOP_EXTEND. A non-read-only scan never sets the VM, + * so a missing fork is harmless here; a later VACUUM will create + * it. + */ + if (rel_read_only) + visibilitymap_pin(relation, BufferGetBlockNumber(buffer), + vmbuffer); + else + (void) visibilitymap_get_status(relation, + BufferGetBlockNumber(buffer), + vmbuffer); params.relation = relation; params.buffer = buffer; @@ -418,7 +436,15 @@ prune_freeze_setup(PruneFreezeParams *params, prstate->buffer = params->buffer; prstate->page = BufferGetPage(params->buffer); - Assert(BufferIsValid(params->vmbuffer)); + /* + * Callers that may set the VM must provide a pinned vmbuffer. On-access + * pruning of a non-read-only scan does not extend the VM fork, so it may + * pass an invalid vmbuffer when the fork does not exist yet (see + * heap_page_prune_opt()); in that case we neither detect VM corruption nor + * set the VM below. + */ + Assert(BufferIsValid(params->vmbuffer) || + params->reason == PRUNE_ON_ACCESS); prstate->vmbuffer = params->vmbuffer; prstate->new_vmbits = 0; prstate->old_vmbits = visibilitymap_get_status(prstate->relation,