Add a test for half-dead pages in B-tree indexes

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
This commit is contained in:
Heikki Linnakangas 2025-12-02 21:11:05 +02:00
parent 6c05ef5729
commit c085aab278
4 changed files with 122 additions and 0 deletions

View 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.

View File

@ -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)

View File

@ -10,6 +10,7 @@ tests += {
'bd': meson.current_build_dir(),
'regress': {
'sql': [
'nbtree_half_dead_pages',
'nbtree_incomplete_splits',
],
},

View File

@ -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;