From 80ecba3bcef1937439d06ac484ecb50fbb328294 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 29 Jul 2026 10:29:10 -0400
Subject: [PATCH v1] Fix autovacuum-induced flakiness in backwards scan test.

Two of the permutations in backwards-scan-concurrent-splits rely on
their VACUUM step deleting the leaf pages that the waiting backwards
scan will have to recover from.  VACUUM can only do that when it's able
to remove the index tuples whose heap tuples the concurrent session just
deleted.  An autovacuum worker holding a snapshot holds back the
removable cutoff, which leaves the pages non-empty, and so undeleted,
causing the test to fail spuriously.

To fix, wait for the removable cutoff to advance past the deletions
before the scan acquires its snapshot.  This is much like commit
1c64d2fc, which dealt with the same hazard in nbtree_half_dead_pages by
adding the wait_prunable() helper that we reuse here.

Oversight in commit e395fbd3.

Author: Peter Geoghegan <pg@bowt.ie>
Reported-by: Alexander Lakhin <exclusion@gmail.com>
Discussion: https://postgr.es/m/b61d9944-d7a3-45f3-b69a-f18c8bfbbbd0@gmail.com
---
 .../backwards-scan-concurrent-splits.out      |  6 +++--
 .../backwards-scan-concurrent-splits.spec     | 24 +++++++++++++++++++
 2 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/src/test/modules/nbtree/expected/backwards-scan-concurrent-splits.out b/src/test/modules/nbtree/expected/backwards-scan-concurrent-splits.out
index 906c10d10..804e690dc 100644
--- a/src/test/modules/nbtree/expected/backwards-scan-concurrent-splits.out
+++ b/src/test/modules/nbtree/expected/backwards-scan-concurrent-splits.out
@@ -162,7 +162,7 @@ injection_points_detach
 (1 row)
 
 
-starting permutation: b_attach d_delete_left b_scan vacuum_tbl i_detach b_detach
+starting permutation: b_attach d_delete_left wait_prunable b_scan vacuum_tbl i_detach b_detach
 step b_attach: 
   SELECT injection_points_attach('nbtree-walk-left', 'wait');
   SELECT injection_points_attach('nbtree-walk-left-step-right', 'notice');
@@ -190,6 +190,7 @@ injection_points_attach
 (1 row)
 
 step d_delete_left: DELETE FROM backwards_scan_tbl WHERE col < 601;
+step wait_prunable: CALL wait_prunable();
 step b_scan: SELECT col FROM backwards_scan_tbl
               WHERE col % 100 = 1 AND pg_backend_pid() <> 0
               ORDER BY col DESC; <waiting ...>
@@ -236,7 +237,7 @@ injection_points_detach
 (1 row)
 
 
-starting permutation: b_attach_nosr i_grow d_delete_mid b_scan_999 vacuum_tbl i_detach b_detach_nosr
+starting permutation: b_attach_nosr i_grow d_delete_mid wait_prunable b_scan_999 vacuum_tbl i_detach b_detach_nosr
 step b_attach_nosr: 
   SELECT injection_points_attach('nbtree-walk-left', 'wait');
   SELECT injection_points_attach('nbtree-walk-left-restart', 'notice');
@@ -259,6 +260,7 @@ injection_points_attach
 
 step i_grow: INSERT INTO backwards_scan_tbl SELECT i FROM generate_series(701, 2200) i;
 step d_delete_mid: DELETE FROM backwards_scan_tbl WHERE col BETWEEN 367 AND 2100;
+step wait_prunable: CALL wait_prunable();
 step b_scan_999: SELECT col FROM backwards_scan_tbl
                   WHERE col <= 999 AND col % 100 = 1 AND pg_backend_pid() <> 0
                   ORDER BY col DESC; <waiting ...>
diff --git a/src/test/modules/nbtree/specs/backwards-scan-concurrent-splits.spec b/src/test/modules/nbtree/specs/backwards-scan-concurrent-splits.spec
index 62c0cf25a..7e35dc883 100644
--- a/src/test/modules/nbtree/specs/backwards-scan-concurrent-splits.spec
+++ b/src/test/modules/nbtree/specs/backwards-scan-concurrent-splits.spec
@@ -21,6 +21,26 @@ setup
   CREATE TABLE backwards_scan_tbl(col int4) WITH (autovacuum_enabled = off);
   CREATE INDEX ON backwards_scan_tbl(col) WITH (deduplicate_items = off);
   INSERT INTO backwards_scan_tbl SELECT i FROM generate_series(0, 700) i;
+  -- Wait until every dead tuple in the table has become removable by VACUUM.
+  -- Autovacuum can hold a snapshot open in any database at any time, which
+  -- holds back the removable cutoff.
+  CREATE PROCEDURE wait_prunable() LANGUAGE plpgsql AS $$
+    DECLARE
+      barrier xid8;
+      cutoff xid8;
+    BEGIN
+      barrier := pg_current_xact_id();
+      -- Pass a shared catalog rather than the table we'll prune, to prevent
+      -- the cutoff from moving backwards.  See comments at removable_cutoff()
+      LOOP
+        ROLLBACK;  -- release MyProc->xmin, which could be the oldest
+        cutoff := removable_cutoff('pg_database');
+        EXIT WHEN cutoff >= barrier;
+        RAISE LOG 'removable cutoff %; waiting for %', cutoff, barrier;
+        PERFORM pg_sleep(.1);
+      END LOOP;
+    END
+  $$;
 }
 setup
 {
@@ -30,6 +50,7 @@ teardown
 {
   DROP EXTENSION injection_points;
   DROP TABLE backwards_scan_tbl;
+  DROP PROCEDURE wait_prunable;
 }
 
 session scan_session
@@ -76,6 +97,7 @@ step i_insert_dups { INSERT INTO backwards_scan_tbl SELECT 100 FROM generate_ser
 step i_grow { INSERT INTO backwards_scan_tbl SELECT i FROM generate_series(701, 2200) i; }
 step d_delete_left { DELETE FROM backwards_scan_tbl WHERE col < 601; }
 step d_delete_mid { DELETE FROM backwards_scan_tbl WHERE col BETWEEN 367 AND 2100; }
+step wait_prunable { CALL wait_prunable(); }
 step vacuum_tbl { VACUUM backwards_scan_tbl; }
 step i_detach {
   SELECT injection_points_detach('nbtree-walk-left');
@@ -104,6 +126,7 @@ permutation b_attach
 # the scan has no page to the left to move to, ending the scan.
 permutation b_attach
     d_delete_left
+    wait_prunable
     b_scan
     vacuum_tbl
     i_detach
@@ -117,6 +140,7 @@ permutation b_attach
 permutation b_attach_nosr
     i_grow
     d_delete_mid
+    wait_prunable
     b_scan_999
     vacuum_tbl
     i_detach
-- 
2.53.0

