From edd7a401e438c4c549037c95e48dcd48a233cb7d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 7 Jul 2025 14:11:17 -0400
Subject: [PATCH 1/3] injection points

---
 src/backend/access/nbtree/nbtsearch.c         |  4 ++
 src/test/modules/Makefile                     |  4 +-
 src/test/modules/meson.build                  |  1 +
 src/test/modules/nbtree/.gitignore            |  4 ++
 src/test/modules/nbtree/Makefile              | 28 +++++++++++
 .../modules/nbtree/expected/backwards.out     | 32 +++++++++++++
 src/test/modules/nbtree/meson.build           | 17 +++++++
 src/test/modules/nbtree/specs/backwards.spec  | 46 +++++++++++++++++++
 8 files changed, 134 insertions(+), 2 deletions(-)
 create mode 100644 src/test/modules/nbtree/.gitignore
 create mode 100644 src/test/modules/nbtree/Makefile
 create mode 100644 src/test/modules/nbtree/expected/backwards.out
 create mode 100644 src/test/modules/nbtree/meson.build
 create mode 100644 src/test/modules/nbtree/specs/backwards.spec

diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 4af1ff1e9..c250a4b92 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -21,6 +21,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "storage/predicate.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
@@ -2500,6 +2501,8 @@ _bt_lock_and_validate_left(Relation rel, BlockNumber *blkno,
 {
 	BlockNumber origblkno = *blkno; /* detects circular links */
 
+	INJECTION_POINT("lock-and-validate-left", NULL);
+
 	for (;;)
 	{
 		Buffer		buf;
@@ -2598,6 +2601,7 @@ _bt_lock_and_validate_left(Relation rel, BlockNumber *blkno,
 		/* Start from scratch with new lastcurrblkno's blkno/prev link */
 		*blkno = origblkno = opaque->btpo_prev;
 		_bt_relbuf(rel, buf);
+		INJECTION_POINT("lock-and-validate-new-lastcurrblkno", NULL);
 	}
 
 	return InvalidBuffer;
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index aa1d27bbe..06b0de718 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -46,9 +46,9 @@ SUBDIRS = \
 
 
 ifeq ($(enable_injection_points),yes)
-SUBDIRS += injection_points gin typcache
+SUBDIRS += injection_points gin nbtree typcache
 else
-ALWAYS_SUBDIRS += injection_points gin typcache
+ALWAYS_SUBDIRS += injection_points gin nbtree typcache
 endif
 
 ifeq ($(with_ssl),openssl)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 9de0057bd..54a3e7ba5 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -9,6 +9,7 @@ subdir('gin')
 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/.gitignore b/src/test/modules/nbtree/.gitignore
new file mode 100644
index 000000000..5dcb3ff97
--- /dev/null
+++ b/src/test/modules/nbtree/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/nbtree/Makefile b/src/test/modules/nbtree/Makefile
new file mode 100644
index 000000000..bdcf80e60
--- /dev/null
+++ b/src/test/modules/nbtree/Makefile
@@ -0,0 +1,28 @@
+# src/test/modules/nbtree/Makefile
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+ISOLATION = backwards
+
+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/backwards.out b/src/test/modules/nbtree/expected/backwards.out
new file mode 100644
index 000000000..6b334a2ac
--- /dev/null
+++ b/src/test/modules/nbtree/expected/backwards.out
@@ -0,0 +1,32 @@
+Parsed test spec with 2 sessions
+
+starting permutation: b_scan i_insert i_detach
+step b_scan: SELECT * FROM backwards_scan_tbl WHERE col % 100 = 1 ORDER BY col DESC; <waiting ...>
+step i_insert: INSERT INTO backwards_scan_tbl SELECT i FROM generate_series(-2000, 700) i;
+step i_detach: 
+  SELECT injection_points_detach('lock-and-validate-left');
+  SELECT injection_points_wakeup('lock-and-validate-left');
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+backwards_scan_session: NOTICE:  notice triggered for injection point lock-and-validate-new-lastcurrblkno
+step b_scan: <... completed>
+col
+---
+601
+501
+401
+301
+201
+101
+  1
+(7 rows)
+
diff --git a/src/test/modules/nbtree/meson.build b/src/test/modules/nbtree/meson.build
new file mode 100644
index 000000000..20d77077b
--- /dev/null
+++ b/src/test/modules/nbtree/meson.build
@@ -0,0 +1,17 @@
+# 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(),
+  'isolation': {
+    'specs': [
+      'backwards',
+    ],
+    'runningcheck': false, # see syscache-update-pruned
+  },
+}
diff --git a/src/test/modules/nbtree/specs/backwards.spec b/src/test/modules/nbtree/specs/backwards.spec
new file mode 100644
index 000000000..780929ee1
--- /dev/null
+++ b/src/test/modules/nbtree/specs/backwards.spec
@@ -0,0 +1,46 @@
+# Backwards scan isolation test
+
+setup
+{
+  CREATE EXTENSION injection_points;
+  CREATE TABLE backwards_scan_tbl(col int4) WITH (autovacuum_enabled = off);
+  CREATE INDEX ON backwards_scan_tbl(col);
+  INSERT INTO backwards_scan_tbl SELECT i FROM generate_series(0, 700) i;
+}
+setup
+{
+  VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) backwards_scan_tbl;
+}
+teardown
+{
+  DROP EXTENSION injection_points;
+  DROP TABLE backwards_scan_tbl;
+}
+
+# Wait happens in backwards_scan_session session, wakeup in insert session
+#
+# The lock-and-validate-new-lastcurrblkno injection point is used for the
+# wait.  The lock-and-validate-left injection point is used to generate a
+# notification that confirms that we have the desired test coverage.
+session backwards_scan_session
+setup {
+  SELECT injection_points_set_local();
+  SELECT injection_points_attach('lock-and-validate-new-lastcurrblkno', 'notice');
+  SELECT injection_points_attach('lock-and-validate-left', 'wait');
+  SET enable_seqscan=off;
+  SET enable_sort=off;
+}
+step b_scan { SELECT * FROM backwards_scan_tbl WHERE col % 100 = 1 ORDER BY col DESC; }
+
+session insert_scan_session
+step i_detach {
+  SELECT injection_points_detach('lock-and-validate-left');
+  SELECT injection_points_wakeup('lock-and-validate-left');
+}
+step i_insert { INSERT INTO backwards_scan_tbl SELECT i FROM generate_series(-2000, 700) i; }
+
+# Start a backwards scan session that waits "between pages".  Meanwhile, a
+# concurrent session performs insertions that cause many page splits.  When
+# the backwards scan session wakes up, it'll have to reason about these
+# concurrent page splits the hard way.
+permutation b_scan i_insert i_detach
-- 
2.50.0

