From 41f55ee2e81c518b6701231336b5eb4ec7534a40 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Tue, 21 Jul 2026 09:24:27 +0500 Subject: [PATCH 2/3] Add isolation test for GIN posting tree page deletion Add two injection points to the vacuum sweep: one between two leaf pages, where no buffer locks or pins are held, and one just before an empty leaf is deleted, at vacuum's maximum lock footprint. The test checks the baseline deletion path, and that searches and insertions into the same posting tree proceed while vacuum is paused mid-sweep. The expected notice counts assume the default 8KB block size, like other tests in this module. --- src/backend/access/gin/ginvacuum.c | 17 +++ src/test/modules/gin/Makefile | 4 +- .../gin/expected/vacuum_posting_tree.out | 131 ++++++++++++++++++ src/test/modules/gin/meson.build | 5 + .../gin/specs/vacuum_posting_tree.spec | 104 ++++++++++++++ 5 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/gin/expected/vacuum_posting_tree.out create mode 100644 src/test/modules/gin/specs/vacuum_posting_tree.spec diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c index ca4c2c4d953..b3b562c078b 100644 --- a/src/backend/access/gin/ginvacuum.c +++ b/src/backend/access/gin/ginvacuum.c @@ -23,6 +23,7 @@ #include "storage/lmgr.h" #include "storage/predicate.h" #include "storage/read_stream.h" +#include "utils/injection_point.h" #include "utils/memutils.h" struct GinVacuumState @@ -420,6 +421,14 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno) vacuum_delay_point(false); + /* + * A wait here can be used to pause the sweep between two leaf + * pages, while no buffer locks or pins are held, letting a + * concurrent session mutate the part of the tree that the sweep + * has not reached yet. + */ + INJECTION_POINT("gin-vacuum-posting-tree-resume", NULL); + prevBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, prevBlkno, RBM_NORMAL, gvs->strategy); LockBuffer(prevBuffer, GIN_EXCLUSIVE); @@ -465,6 +474,14 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno) parentBuffer = ginLockLeafParent(gvs, &parentBlkno, blkno, &off); if (BufferIsValid(parentBuffer)) { + /* + * This fires with the two leaf pages and the parent page + * all exclusively locked. A 'wait' attached here pauses + * vacuum at its maximum lock footprint, blocking any + * concurrent descent through the parent page. + */ + INJECTION_POINT("gin-vacuum-delete-posting-page", NULL); + ginDeletePostingPage(gvs, buffer, prevBuffer, parentBuffer, off); UnlockReleaseBuffer(parentBuffer); diff --git a/src/test/modules/gin/Makefile b/src/test/modules/gin/Makefile index e007e38ac27..6c04a3d6fc8 100644 --- a/src/test/modules/gin/Makefile +++ b/src/test/modules/gin/Makefile @@ -1,9 +1,11 @@ # src/test/modules/gin/Makefile -EXTRA_INSTALL = src/test/modules/injection_points +EXTRA_INSTALL = src/test/modules/injection_points contrib/amcheck REGRESS = gin_incomplete_splits +ISOLATION = vacuum_posting_tree + ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/src/test/modules/gin/expected/vacuum_posting_tree.out b/src/test/modules/gin/expected/vacuum_posting_tree.out new file mode 100644 index 00000000000..9213de1494c --- /dev/null +++ b/src/test/modules/gin/expected/vacuum_posting_tree.out @@ -0,0 +1,131 @@ +Parsed test spec with 2 sessions + +starting permutation: v_attach_notice v_delete v_vacuum v_detach_notice c_count c_check +injection_points_set_local +-------------------------- + +(1 row) + +step v_attach_notice: + SELECT injection_points_attach('gin-vacuum-delete-posting-page', 'notice'); + +injection_points_attach +----------------------- + +(1 row) + +step v_delete: + DELETE FROM gin_pt WHERE k BETWEEN 5000 AND 25000; + +vacuumer: NOTICE: notice triggered for injection point gin-vacuum-delete-posting-page +vacuumer: NOTICE: notice triggered for injection point gin-vacuum-delete-posting-page +vacuumer: NOTICE: notice triggered for injection point gin-vacuum-delete-posting-page +step v_vacuum: + VACUUM gin_pt; + +step v_detach_notice: + SELECT injection_points_detach('gin-vacuum-delete-posting-page'); + +injection_points_detach +----------------------- + +(1 row) + +step c_count: + SELECT count(*) FROM gin_pt WHERE i @> array[1]; + +count +----- + 9999 +(1 row) + +step c_check: + SELECT gin_index_check('gin_pt_idx'); + +gin_index_check +--------------- + +(1 row) + + +starting permutation: v_attach_notice v_attach_wait v_delete v_vacuum c_count c_insert c_detach_wake v_detach_notice c_count c_check +injection_points_set_local +-------------------------- + +(1 row) + +step v_attach_notice: + SELECT injection_points_attach('gin-vacuum-delete-posting-page', 'notice'); + +injection_points_attach +----------------------- + +(1 row) + +step v_attach_wait: + SELECT injection_points_attach('gin-vacuum-posting-tree-resume', 'wait'); + +injection_points_attach +----------------------- + +(1 row) + +step v_delete: + DELETE FROM gin_pt WHERE k BETWEEN 5000 AND 25000; + +step v_vacuum: + VACUUM gin_pt; + +step c_count: + SELECT count(*) FROM gin_pt WHERE i @> array[1]; + +count +----- + 9999 +(1 row) + +step c_insert: + INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 1000); + +step c_detach_wake: + SELECT injection_points_detach('gin-vacuum-posting-tree-resume'); + SELECT injection_points_wakeup('gin-vacuum-posting-tree-resume'); + +injection_points_detach +----------------------- + +(1 row) + +injection_points_wakeup +----------------------- + +(1 row) + +vacuumer: NOTICE: notice triggered for injection point gin-vacuum-delete-posting-page +vacuumer: NOTICE: notice triggered for injection point gin-vacuum-delete-posting-page +vacuumer: NOTICE: notice triggered for injection point gin-vacuum-delete-posting-page +step v_vacuum: <... completed> +step v_detach_notice: + SELECT injection_points_detach('gin-vacuum-delete-posting-page'); + +injection_points_detach +----------------------- + +(1 row) + +step c_count: + SELECT count(*) FROM gin_pt WHERE i @> array[1]; + +count +----- +10999 +(1 row) + +step c_check: + SELECT gin_index_check('gin_pt_idx'); + +gin_index_check +--------------- + +(1 row) + diff --git a/src/test/modules/gin/meson.build b/src/test/modules/gin/meson.build index ae2c1abe368..55eff0aeb44 100644 --- a/src/test/modules/gin/meson.build +++ b/src/test/modules/gin/meson.build @@ -13,4 +13,9 @@ tests += { 'gin_incomplete_splits', ], }, + 'isolation': { + 'specs': [ + 'vacuum_posting_tree', + ], + }, } diff --git a/src/test/modules/gin/specs/vacuum_posting_tree.spec b/src/test/modules/gin/specs/vacuum_posting_tree.spec new file mode 100644 index 00000000000..5ad014def04 --- /dev/null +++ b/src/test/modules/gin/specs/vacuum_posting_tree.spec @@ -0,0 +1,104 @@ +# Test deletion of empty GIN posting tree pages during vacuum, running +# concurrently with readers and writers of the same posting tree. +# +# Vacuum deletes empty posting tree leaf pages on the fly during its +# left-to-right sweep of the leaf level, without locking out concurrent +# insertions into the tree (see "Page deletion" in the GIN README). +# This test uses two injection points: +# +# - gin-vacuum-posting-tree-resume fires between two leaf pages of the +# sweep, while no buffer locks or pins are held, so it can be used as +# a 'wait' point to pause vacuum in the middle of a posting tree. +# +# - gin-vacuum-delete-posting-page fires when an empty leaf page is +# about to be deleted, with the leaf, its left sibling and its parent +# all exclusively locked. A 'wait' here pauses vacuum at its maximum +# lock footprint. +# +# The expected notice counts assume the default 8KB BLCKSZ: the posting +# tree built by the setup has 8 pages, of which 3 become empty and +# deletable after the DELETE. +# +# All rows carry the single array element 1, so the index has one entry +# tree key whose TIDs form one posting tree. fastupdate is disabled to +# keep the pending list out of the picture. + +setup +{ + CREATE EXTENSION injection_points; + CREATE EXTENSION amcheck; + CREATE TABLE gin_pt(k serial, i int4[]) WITH (autovacuum_enabled = off); + CREATE INDEX gin_pt_idx ON gin_pt USING gin(i) WITH (fastupdate = off); + INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 30000); +} + +teardown +{ + DROP TABLE gin_pt; + DROP EXTENSION amcheck; + DROP EXTENSION injection_points; +} + +session vacuumer +setup +{ + SELECT injection_points_set_local(); +} +step v_attach_notice +{ + SELECT injection_points_attach('gin-vacuum-delete-posting-page', 'notice'); +} +step v_attach_wait +{ + SELECT injection_points_attach('gin-vacuum-posting-tree-resume', 'wait'); +} +step v_delete +{ + DELETE FROM gin_pt WHERE k BETWEEN 5000 AND 25000; +} +step v_vacuum +{ + VACUUM gin_pt; +} +step v_detach_notice +{ + SELECT injection_points_detach('gin-vacuum-delete-posting-page'); +} + +session checker +setup +{ + SET enable_seqscan = off; +} +step c_count +{ + SELECT count(*) FROM gin_pt WHERE i @> array[1]; +} +step c_insert +{ + INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 1000); +} +# Detach before wakeup: the current waiter is woken up, and the sweep +# does not wait at any of the remaining between-pages points. +step c_detach_wake +{ + SELECT injection_points_detach('gin-vacuum-posting-tree-resume'); + SELECT injection_points_wakeup('gin-vacuum-posting-tree-resume'); +} +step c_check +{ + SELECT gin_index_check('gin_pt_idx'); +} + +# Baseline: vacuum deletes the emptied middle pages of the posting tree +# (3 notices), searches keep working, and the index structure is sound. +permutation v_attach_notice v_delete v_vacuum v_detach_notice c_count c_check + +# Pause vacuum between two leaf pages of the sweep, before it has +# deleted anything. While vacuum sleeps inside the posting tree, a +# concurrent session can both search the tree and insert into it: with +# the pre-v19 protocol the insert (and, during the deletion pass, also +# the search) would have blocked behind vacuum's cleanup lock on the +# posting tree root. Then release vacuum and let it finish deleting +# the empty pages (3 notices). +permutation v_attach_notice v_attach_wait v_delete v_vacuum c_count c_insert c_detach_wake v_detach_notice c_count c_check -- 2.50.1 (Apple Git-155)