Add a test for half-dead pages in B-tree indexes
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>
Tue, 2 Dec 2025 19:11:05 +0000 (21:11 +0200)
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>
Tue, 2 Dec 2025 19:11:05 +0000 (21:11 +0200)
To increase our test coverage in general, and because I will use this
in the next commit to test a bug we currently have in amcheck.

Reviewed-by: Peter Geoghegan <pg@bowt.ie>
Discussion: https://www.postgresql.org/message-id/33e39552-6a2a-46f3-8b34-3f9f8004451f@garret.ru

src/backend/access/nbtree/nbtpage.c
src/test/modules/nbtree/expected/nbtree_half_dead_pages.out [new file with mode: 0644]
src/test/modules/nbtree/meson.build
src/test/modules/nbtree/sql/nbtree_half_dead_pages.sql [new file with mode: 0644]

index 30b43a4dd18de17ab30deb1db29608262ad2cde1..a8d56fe5a7caef34ef88fd0651681995e82e7203 100644 (file)
@@ -33,6 +33,7 @@
 #include "storage/indexfsm.h"
 #include "storage/predicate.h"
 #include "storage/procarray.h"
+#include "utils/injection_point.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
 #include "utils/snapmgr.h"
@@ -2003,6 +2004,10 @@ _bt_pagedel(Relation rel, Buffer leafbuf, BTVacState *vstate)
                return;
            }
        }
+       else
+       {
+           INJECTION_POINT("nbtree-finish-half-dead-page-vacuum", NULL);
+       }
 
        /*
         * Then unlink it from its siblings.  Each call to
@@ -2349,6 +2354,8 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno,
 
    _bt_unlockbuf(rel, leafbuf);
 
+   INJECTION_POINT("nbtree-leave-page-half-dead", NULL);
+
    /*
     * Check here, as calling loops will have locks held, preventing
     * interrupts from being processed.
diff --git a/src/test/modules/nbtree/expected/nbtree_half_dead_pages.out b/src/test/modules/nbtree/expected/nbtree_half_dead_pages.out
new file mode 100644 (file)
index 0000000..8fd472f
--- /dev/null
@@ -0,0 +1,71 @@
+--
+-- Test half-dead pages in B-tree indexes.
+--
+-- Half-dead pages is an intermediate state while vacuum is deleting a
+-- page. You can encounter them if you query concurrently with vacuum,
+-- or if vacuum is interrupted while it's deleting a page. A B-tree
+-- with half-dead pages is a valid state, but they rarely observed by
+-- other backends in practice because, so it's good to have some
+-- targeted tests to exercise them.
+--
+-- This uses injection points to interrupt some page deletions
+set client_min_messages TO 'warning';
+create extension if not exists injection_points;
+reset client_min_messages;
+-- Make all injection points local to this process, for concurrency.
+SELECT injection_points_set_local();
+ injection_points_set_local 
+----------------------------
+(1 row)
+
+-- Use the index for all the queries
+set enable_seqscan=off;
+-- Print a NOTICE whenever a half-dead page is deleted
+SELECT injection_points_attach('nbtree-finish-half-dead-page-vacuum', 'notice');
+ injection_points_attach 
+-------------------------
+(1 row)
+
+create table nbtree_half_dead_pages(id bigint) with (autovacuum_enabled = off);
+insert into nbtree_half_dead_pages SELECT g from generate_series(1, 150000) g;
+create index nbtree_half_dead_pages_id_idx on nbtree_half_dead_pages using btree (id);
+delete from nbtree_half_dead_pages where id > 100000 and id < 120000;
+-- Run VACUUM and interrupt it so that it leaves behind a half-dead page
+SELECT injection_points_attach('nbtree-leave-page-half-dead', 'error');
+ injection_points_attach 
+-------------------------
+(1 row)
+
+vacuum nbtree_half_dead_pages;
+ERROR:  error triggered for injection point nbtree-leave-page-half-dead
+CONTEXT:  while vacuuming index "nbtree_half_dead_pages_id_idx" of relation "public.nbtree_half_dead_pages"
+SELECT injection_points_detach('nbtree-leave-page-half-dead');
+ injection_points_detach 
+-------------------------
+(1 row)
+
+select * from nbtree_half_dead_pages where id > 99998 and id < 120002;
+   id   
+--------
+  99999
+ 100000
+ 120000
+ 120001
+(4 rows)
+
+-- Finish the deletion and re-check
+vacuum nbtree_half_dead_pages;
+NOTICE:  notice triggered for injection point nbtree-finish-half-dead-page-vacuum
+select * from nbtree_half_dead_pages where id > 99998 and id < 120002;
+   id   
+--------
+  99999
+ 100000
+ 120000
+ 120001
+(4 rows)
+
index efebf30a16fce83ed69e5e272a797af57d705472..1b9a34d37ca5ea0b09e720303e8541c534d44f91 100644 (file)
@@ -10,6 +10,7 @@ tests += {
   'bd': meson.current_build_dir(),
   'regress': {
     'sql': [
+      'nbtree_half_dead_pages',
       'nbtree_incomplete_splits',
     ],
   },
diff --git a/src/test/modules/nbtree/sql/nbtree_half_dead_pages.sql b/src/test/modules/nbtree/sql/nbtree_half_dead_pages.sql
new file mode 100644 (file)
index 0000000..d4b9a3f
--- /dev/null
@@ -0,0 +1,43 @@
+--
+-- Test half-dead pages in B-tree indexes.
+--
+-- Half-dead pages is an intermediate state while vacuum is deleting a
+-- page. You can encounter them if you query concurrently with vacuum,
+-- or if vacuum is interrupted while it's deleting a page. A B-tree
+-- with half-dead pages is a valid state, but they rarely observed by
+-- other backends in practice because, so it's good to have some
+-- targeted tests to exercise them.
+--
+
+-- This uses injection points to interrupt some page deletions
+set client_min_messages TO 'warning';
+create extension if not exists injection_points;
+reset client_min_messages;
+
+-- Make all injection points local to this process, for concurrency.
+SELECT injection_points_set_local();
+
+-- Use the index for all the queries
+set enable_seqscan=off;
+
+-- Print a NOTICE whenever a half-dead page is deleted
+SELECT injection_points_attach('nbtree-finish-half-dead-page-vacuum', 'notice');
+
+create table nbtree_half_dead_pages(id bigint) with (autovacuum_enabled = off);
+
+insert into nbtree_half_dead_pages SELECT g from generate_series(1, 150000) g;
+
+create index nbtree_half_dead_pages_id_idx on nbtree_half_dead_pages using btree (id);
+
+delete from nbtree_half_dead_pages where id > 100000 and id < 120000;
+
+-- Run VACUUM and interrupt it so that it leaves behind a half-dead page
+SELECT injection_points_attach('nbtree-leave-page-half-dead', 'error');
+vacuum nbtree_half_dead_pages;
+SELECT injection_points_detach('nbtree-leave-page-half-dead');
+
+select * from nbtree_half_dead_pages where id > 99998 and id < 120002;
+
+-- Finish the deletion and re-check
+vacuum nbtree_half_dead_pages;
+select * from nbtree_half_dead_pages where id > 99998 and id < 120002;