diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/backend/access/nbtree/nbtinsert.c | 9 | ||||
| -rw-r--r-- | src/test/modules/meson.build | 1 | ||||
| -rw-r--r-- | src/test/modules/nbtree/Makefile | 28 | ||||
| -rw-r--r-- | src/test/modules/nbtree/expected/nbtree_incomplete_splits.out | 179 | ||||
| -rw-r--r-- | src/test/modules/nbtree/meson.build | 16 | ||||
| -rw-r--r-- | src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql | 134 |
6 files changed, 367 insertions, 0 deletions
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c index 7c113c007e5..3a4b791f2ab 100644 --- a/src/backend/access/nbtree/nbtinsert.c +++ b/src/backend/access/nbtree/nbtinsert.c @@ -26,6 +26,7 @@ #include "miscadmin.h" #include "storage/lmgr.h" #include "storage/predicate.h" +#include "utils/injection_point.h" /* Minimum tree height for application of fastpath optimization */ #define BTREE_FASTPATH_MIN_LEVEL 2 @@ -1239,6 +1240,13 @@ _bt_insertonpg(Relation rel, * page. *---------- */ +#ifdef USE_INJECTION_POINTS + if (P_ISLEAF(opaque)) + INJECTION_POINT("nbtree-leave-leaf-split-incomplete", NULL); + else + INJECTION_POINT("nbtree-leave-internal-split-incomplete", NULL); +#endif + _bt_insert_parent(rel, heaprel, buf, rbuf, stack, isroot, isonly); } else @@ -2285,6 +2293,7 @@ _bt_finish_split(Relation rel, Relation heaprel, Buffer lbuf, BTStack stack) /* Was this the only page on the level before split? */ wasonly = (P_LEFTMOST(lpageop) && P_RIGHTMOST(rpageop)); + INJECTION_POINT("nbtree-finish-incomplete-split", NULL); elog(DEBUG1, "finishing incomplete split of %u/%u", BufferGetBlockNumber(lbuf), BufferGetBlockNumber(rbuf)); diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index f5114469b92..cc57461e59a 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -10,6 +10,7 @@ subdir('index') subdir('injection_points') subdir('ldap_password_func') subdir('libpq_pipeline') +subdir('nbtree') subdir('oauth_validator') subdir('plsample') subdir('spgist_name_ops') diff --git a/src/test/modules/nbtree/Makefile b/src/test/modules/nbtree/Makefile new file mode 100644 index 00000000000..34946a84445 --- /dev/null +++ b/src/test/modules/nbtree/Makefile @@ -0,0 +1,28 @@ +# src/test/modules/nbtree/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +REGRESS = nbtree_incomplete_splits + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/nbtree +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global + +# XXX: This test is conditional on enable_injection_points in the +# parent Makefile, so we should never get here in the first place if +# injection points are not enabled. But the buildfarm 'misc-check' +# step doesn't pay attention to the if-condition in the parent +# Makefile. To work around that, disable running the test here too. +ifeq ($(enable_injection_points),yes) +include $(top_srcdir)/contrib/contrib-global.mk +else +check: + @echo "injection points are disabled in this build" +endif + +endif diff --git a/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out b/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out new file mode 100644 index 00000000000..88e87e875c8 --- /dev/null +++ b/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out @@ -0,0 +1,179 @@ +-- +-- Test incomplete splits in B-tree indexes. +-- +-- We use a test table with integers from 1 to :next_i. Each integer +-- occurs exactly once, no gaps or duplicates, although the index does +-- contain some duplicates because some of the inserting transactions +-- are rolled back during the test. The exact contents of the table +-- depend on the physical layout of the index, which in turn depends +-- at least on the block size, so instead of checking the exact +-- contents, we check those invariants. :next_i psql variable is +-- maintained at all times to hold the last inserted integer + 1. +-- +-- This uses injection points to cause errors that leave some page +-- splits in "incomplete" state +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 an incomplete split gets fixed +SELECT injection_points_attach('nbtree-finish-incomplete-split', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +-- +-- First create the test table and some helper functions +-- +create table nbtree_incomplete_splits(i int4) with (autovacuum_enabled = off); +create index nbtree_incomplete_splits_i_idx on nbtree_incomplete_splits using btree (i); +-- Inserts 'n' rows to the test table. Pass :next_i as the first +-- argument, returns the new value for :next_i. +create function insert_n(first_i int, n int) returns int language plpgsql as $$ +begin + insert into nbtree_incomplete_splits select g from generate_series(first_i, first_i + n - 1) as g; + return first_i + n; +end; +$$; +-- Inserts to the table until an insert fails. Like insert_n(), returns the +-- new value for :next_i. +create function insert_until_fail(next_i int, step int default 1) returns int language plpgsql as $$ +declare + i integer; +begin + -- Insert rows in batches of 'step' rows each, until an error occurs. + i := 0; + loop + begin + select insert_n(next_i, step) into next_i; + exception when others then + raise notice 'failed with: %', sqlerrm; + exit; + end; + + -- The caller is expected to set an injection point that eventually + -- causes an error. But bail out if still no error after 10000 + -- attempts, so that we don't get stuck in an infinite loop. + i := i + 1; + if i = 10000 then + raise 'no error on inserts after % iterations', i; + end if; + end loop; + + return next_i; +end; +$$; +-- Check the invariants. +create function verify(next_i int) returns bool language plpgsql as $$ +declare + c integer; +begin + -- Perform a scan over the trailing part of the index, where the + -- possible incomplete splits are. (We don't check the whole table, + -- because that'd be pretty slow.) + -- + -- Find all rows that overlap with the last 200 inserted integers. Or + -- the next 100, which shouldn't exist. + select count(*) into c from nbtree_incomplete_splits where i between next_i - 200 and next_i + 100; + if c <> 200 then + raise 'unexpected count % ', c; + end if; + return true; +end; +$$; +-- Insert one array to get started. +select insert_n(1, 1000) as next_i +\gset +select verify(:next_i); + verify +-------- + t +(1 row) + +-- +-- Test incomplete leaf split +-- +SELECT injection_points_attach('nbtree-leave-leaf-split-incomplete', 'error'); + injection_points_attach +------------------------- + +(1 row) + +select insert_until_fail(:next_i) as next_i +\gset +NOTICE: failed with: error triggered for injection point nbtree-leave-leaf-split-incomplete +SELECT injection_points_detach('nbtree-leave-leaf-split-incomplete'); + injection_points_detach +------------------------- + +(1 row) + +-- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + verify +-------- + t +(1 row) + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +NOTICE: notice triggered for injection point nbtree-finish-incomplete-split +-- Verify that a scan still works +select verify(:next_i); + verify +-------- + t +(1 row) + +-- +-- Test incomplete internal page split +-- +SELECT injection_points_attach('nbtree-leave-internal-split-incomplete', 'error'); + injection_points_attach +------------------------- + +(1 row) + +select insert_until_fail(:next_i, 100) as next_i +\gset +NOTICE: failed with: error triggered for injection point nbtree-leave-internal-split-incomplete +SELECT injection_points_detach('nbtree-leave-internal-split-incomplete'); + injection_points_detach +------------------------- + +(1 row) + + -- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + verify +-------- + t +(1 row) + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +NOTICE: notice triggered for injection point nbtree-finish-incomplete-split +-- Verify that a scan still works +select verify(:next_i); + verify +-------- + t +(1 row) + +SELECT injection_points_detach('nbtree-finish-incomplete-split'); + injection_points_detach +------------------------- + +(1 row) + diff --git a/src/test/modules/nbtree/meson.build b/src/test/modules/nbtree/meson.build new file mode 100644 index 00000000000..efebf30a16f --- /dev/null +++ b/src/test/modules/nbtree/meson.build @@ -0,0 +1,16 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not get_option('injection_points') + subdir_done() +endif + +tests += { + 'name': 'nbtree', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'nbtree_incomplete_splits', + ], + }, +} diff --git a/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql b/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql new file mode 100644 index 00000000000..0609ed7464e --- /dev/null +++ b/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql @@ -0,0 +1,134 @@ +-- +-- Test incomplete splits in B-tree indexes. +-- +-- We use a test table with integers from 1 to :next_i. Each integer +-- occurs exactly once, no gaps or duplicates, although the index does +-- contain some duplicates because some of the inserting transactions +-- are rolled back during the test. The exact contents of the table +-- depend on the physical layout of the index, which in turn depends +-- at least on the block size, so instead of checking the exact +-- contents, we check those invariants. :next_i psql variable is +-- maintained at all times to hold the last inserted integer + 1. +-- + +-- This uses injection points to cause errors that leave some page +-- splits in "incomplete" state +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 an incomplete split gets fixed +SELECT injection_points_attach('nbtree-finish-incomplete-split', 'notice'); + +-- +-- First create the test table and some helper functions +-- +create table nbtree_incomplete_splits(i int4) with (autovacuum_enabled = off); + +create index nbtree_incomplete_splits_i_idx on nbtree_incomplete_splits using btree (i); + +-- Inserts 'n' rows to the test table. Pass :next_i as the first +-- argument, returns the new value for :next_i. +create function insert_n(first_i int, n int) returns int language plpgsql as $$ +begin + insert into nbtree_incomplete_splits select g from generate_series(first_i, first_i + n - 1) as g; + return first_i + n; +end; +$$; + +-- Inserts to the table until an insert fails. Like insert_n(), returns the +-- new value for :next_i. +create function insert_until_fail(next_i int, step int default 1) returns int language plpgsql as $$ +declare + i integer; +begin + -- Insert rows in batches of 'step' rows each, until an error occurs. + i := 0; + loop + begin + select insert_n(next_i, step) into next_i; + exception when others then + raise notice 'failed with: %', sqlerrm; + exit; + end; + + -- The caller is expected to set an injection point that eventually + -- causes an error. But bail out if still no error after 10000 + -- attempts, so that we don't get stuck in an infinite loop. + i := i + 1; + if i = 10000 then + raise 'no error on inserts after % iterations', i; + end if; + end loop; + + return next_i; +end; +$$; + +-- Check the invariants. +create function verify(next_i int) returns bool language plpgsql as $$ +declare + c integer; +begin + -- Perform a scan over the trailing part of the index, where the + -- possible incomplete splits are. (We don't check the whole table, + -- because that'd be pretty slow.) + -- + -- Find all rows that overlap with the last 200 inserted integers. Or + -- the next 100, which shouldn't exist. + select count(*) into c from nbtree_incomplete_splits where i between next_i - 200 and next_i + 100; + if c <> 200 then + raise 'unexpected count % ', c; + end if; + return true; +end; +$$; + +-- Insert one array to get started. +select insert_n(1, 1000) as next_i +\gset +select verify(:next_i); + + +-- +-- Test incomplete leaf split +-- +SELECT injection_points_attach('nbtree-leave-leaf-split-incomplete', 'error'); +select insert_until_fail(:next_i) as next_i +\gset +SELECT injection_points_detach('nbtree-leave-leaf-split-incomplete'); + +-- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +-- Verify that a scan still works +select verify(:next_i); + + +-- +-- Test incomplete internal page split +-- +SELECT injection_points_attach('nbtree-leave-internal-split-incomplete', 'error'); +select insert_until_fail(:next_i, 100) as next_i +\gset +SELECT injection_points_detach('nbtree-leave-internal-split-incomplete'); + + -- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +-- Verify that a scan still works +select verify(:next_i); + +SELECT injection_points_detach('nbtree-finish-incomplete-split'); |
