Hi, While comparing the vacuum loops of the various index AMs I noticed that gistvacuum_delete_empty_pages() -- the second pass of a GiST vacuum that unlinks empty leaf pages -- does not call vacuum_delay_point(), even though it reads a buffer and issues WAL-logged page deletions for every internal page it revisits. The other page-scanning loops in gistvacuum.c, gistvacuumscan() and the recursion in gistvacuumpage(), both call it.
As a result this phase of a GiST vacuum ignores the cost-based vacuum delay entirely, and only reacts to query cancellation when a buffer read happens to perform I/O. On a large GiST index with many emptied leaf pages this loop can end up walking every internal page of the index. The attached patch adds a single vacuum_delay_point(false) at the top of the loop, where no buffer lock is held, matching the sibling idiom. The function has lacked the call since it was introduced in 7df159a620b. This is long-standing rather than a recent regression, so I'll leave the question of back-patching to a committer. Note that on branches before 18 vacuum_delay_point() takes no argument (the bool was added by e5b0b0ce150), so a back-patch would drop the "false". Thanks, Paul Kim
From b611e157862cce7bd9b141427dcc30d8a26e5d6c Mon Sep 17 00:00:00 2001 From: Paul Kim <[email protected]> Date: Tue, 21 Jul 2026 18:30:41 +0900 Subject: [PATCH v1] Add vacuum_delay_point() to GiST empty-page deletion pass gistvacuum_delete_empty_pages() rescans the index's internal pages to unlink the empty leaf pages found during the main vacuum scan. For each internal page it reads a buffer and may perform WAL-logged page deletions, but the loop never calls vacuum_delay_point(). This phase of a GiST vacuum therefore ignores the cost-based vacuum delay, unlike the other page-scanning loops in this file: gistvacuumscan() calls vacuum_delay_point() at the top of its per-page loop, and gistvacuumpage() calls it before re-reading a buffer when it recurses to an earlier page. Add vacuum_delay_point(false) at the top of the loop, where no buffer lock is held, matching the sibling idiom. vacuum_delay_point() also checks for interrupts, so this additionally lets the pass respond to query cancellation without having to wait for a buffer read to perform I/O. gistvacuum_delete_empty_pages() has lacked this since it was introduced in commit 7df159a620b. --- src/backend/access/gist/gistvacuum.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c index 686a0418054..6637b60d600 100644 --- a/src/backend/access/gist/gistvacuum.c +++ b/src/backend/access/gist/gistvacuum.c @@ -524,6 +524,9 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate) int ntodelete; int deleted; + /* call vacuum_delay_point while not holding any buffer lock */ + vacuum_delay_point(false); + buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno, RBM_NORMAL, info->strategy); -- 2.50.1 (Apple Git-155)
