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)

Reply via email to