summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Geoghegan2026-08-19 17:46:36 +0000
committerPeter Geoghegan2026-08-19 17:46:36 +0000
commit76c05f5080c8c0b285247e58f7c2303a647646e6 (patch)
tree570c47d4d2b23ce6f9a1c1cee9d5bbb1912499fd
parent07615108062b2c271b4697ba68fa8c11335ecbce (diff)
Fix GIN multiple-VACUUM-scans pending list bug.
ginbulkdelete performs pending list cleanup before it searches the entry tree (and any posting trees) for dead TIDs. This is necessary to avoid leaving behind dangling TID references that index vacuuming is required to remove; nothing prevents recently inserted pending list tuples from containing TIDs that VACUUM already considers dead. However, ginbulkdelete neglected to perform pending list cleanup on VACUUM's second or subsequent call. It was therefore possible for a VACUUM that requires multiple rounds of index vacuuming to leave behind dangling references. To fix, teach ginbulkdelete to perform pending list cleanup during every call. In passing, tweak some related comments in the pending list cleanup path to make it clear why it's safe for VACUUM to not _fully_ empty an index's pending list. This was arguably an oversight in commit e2c79e14, which fixed a similar issue where pending list cleanup by VACUUM could end early, but missed this closely related problem. Author: Peter Geoghegan <pg@bowt.ie> Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru> Discussion: https://postgr.es/m/CAH2-Wzmsa-RPA2Ko8A5LaGOnmbpimJ--71xkiBqwgjk3Fq8YEg@mail.gmail.com Backpatch-through: 14
-rw-r--r--src/backend/access/gin/ginfast.c15
-rw-r--r--src/backend/access/gin/ginvacuum.c18
-rw-r--r--src/include/access/gin_private.h2
3 files changed, 22 insertions, 13 deletions
diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c
index e5074659d82..1d3f8aa970b 100644
--- a/src/backend/access/gin/ginfast.c
+++ b/src/backend/access/gin/ginfast.c
@@ -779,7 +779,7 @@ processPendingPage(BuildAccumulator *accum, KeyArray *ka,
* If stats isn't null, we count deleted pending pages into the counts.
*/
void
-ginInsertCleanup(GinState *ginstate, bool full_clean,
+ginInsertCleanup(GinState *ginstate, bool must_empty_list,
bool fill_fsm, bool forceCleanup,
IndexBulkDeleteResult *stats)
{
@@ -810,7 +810,9 @@ ginInsertCleanup(GinState *ginstate, bool full_clean,
{
/*
* We are called from [auto]vacuum/analyze or gin_clean_pending_list()
- * and we would like to wait concurrent cleanup to finish.
+ * and we must wait for concurrent cleanup to finish. In particular,
+ * VACUUM must have the opportunity to remove any dead TIDs that are
+ * now in the pending list.
*/
LockPage(index, GIN_METAPAGE_BLKNO, ExclusiveLock);
workMemory =
@@ -882,11 +884,12 @@ ginInsertCleanup(GinState *ginstate, bool full_clean,
/*
* Are we walk through the page which as we remember was a tail when
- * we start our cleanup? But if caller asks us to clean up whole
- * pending list then ignore old tail, we will work until list becomes
- * empty.
+ * we start our cleanup? But if caller asks us to fully empty the
+ * pending list (not just move all items that were in the list when
+ * blknoFinish was established) then ignore old tail and work until
+ * the list is fully empty.
*/
- if (blkno == blknoFinish && full_clean == false)
+ if (blkno == blknoFinish && !must_empty_list)
cleanupFinish = true;
/*
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index f4d0e03bd51..449b4676cec 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -603,14 +603,20 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
{
/* Yes, so initialize stats to zeroes */
stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
-
- /*
- * and cleanup any pending inserts
- */
- ginInsertCleanup(&gvs.ginstate, !IsAutoVacuumWorkerProcess(),
- false, true, stats);
}
+ /*
+ * The pending list might have already-dead TIDs that VACUUM now requires
+ * us to remove from the index. We must force cleanup of the pending list
+ * now, before vacuuming proper begins, to make sure nothing is missed.
+ *
+ * When running in an autovacuum worker, we won't necessarily _fully_
+ * empty the pending list. This is still safe; concurrent inserters
+ * cannot insert new tuples whose TIDs VACUUM needs us to remove.
+ */
+ ginInsertCleanup(&gvs.ginstate, !IsAutoVacuumWorkerProcess(),
+ false, true, stats);
+
/* we'll re-count the tuples each time */
stats->num_index_tuples = 0;
gvs.result = stats;
diff --git a/src/include/access/gin_private.h b/src/include/access/gin_private.h
index 2935d2f353c..11112b3301f 100644
--- a/src/include/access/gin_private.h
+++ b/src/include/access/gin_private.h
@@ -462,7 +462,7 @@ extern void ginHeapTupleFastCollect(GinState *ginstate,
GinTupleCollector *collector,
OffsetNumber attnum, Datum value, bool isNull,
ItemPointer ht_ctid);
-extern void ginInsertCleanup(GinState *ginstate, bool full_clean,
+extern void ginInsertCleanup(GinState *ginstate, bool must_empty_list,
bool fill_fsm, bool forceCleanup, IndexBulkDeleteResult *stats);
/* ginpostinglist.c */