{
BlockNumber origblkno = *blkno; /* detects circular links */
+#ifdef USE_INJECTION_POINTS
+ if (!IsCatalogRelation(rel))
+ INJECTION_POINT("nbtree-walk-left", NULL);
+#endif
+
for (;;)
{
Buffer buf;
}
if (P_RIGHTMOST(opaque) || ++tries > 4)
break;
+
+#ifdef USE_INJECTION_POINTS
+ if (!IsCatalogRelation(rel))
+ INJECTION_POINT("nbtree-walk-left-step-right", NULL);
+#endif
+
/* step right */
*blkno = opaque->btpo_next;
buf = _bt_relandgetbuf(rel, buf, *blkno, BT_READ);
opaque = BTPageGetOpaque(page);
if (P_ISDELETED(opaque))
{
+#ifdef USE_INJECTION_POINTS
+ if (!IsCatalogRelation(rel))
+ INJECTION_POINT("nbtree-walk-left-deleted", NULL);
+#endif
+
/*
* It was deleted. Move right to first nondeleted page (there
* must be one); that is the page that has acquired the deleted
/* Start from scratch with new lastcurrblkno's blkno/prev link */
*blkno = origblkno = opaque->btpo_prev;
_bt_relbuf(rel, buf);
+
+#ifdef USE_INJECTION_POINTS
+ if (!IsCatalogRelation(rel))
+ INJECTION_POINT("nbtree-walk-left-restart", NULL);
+#endif
}
return InvalidBuffer;
REGRESS = nbtree_half_dead_pages \
nbtree_incomplete_splits
-ISOLATION = predicate-empty-index
+ISOLATION = backwards-scan-concurrent-splits \
+ predicate-empty-index
ifdef USE_PGXS
PG_CONFIG = pg_config
--- /dev/null
+Parsed test spec with 2 sessions
+
+starting permutation: b_attach b_scan i_insert_dups 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');
+ SELECT injection_points_attach('nbtree-walk-left-restart', 'notice');
+ SELECT injection_points_attach('nbtree-walk-left-deleted', 'notice');
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+step b_scan: SELECT col FROM backwards_scan_tbl
+ WHERE col % 100 = 1 AND pg_backend_pid() <> 0
+ ORDER BY col DESC; <waiting ...>
+step i_insert_dups: INSERT INTO backwards_scan_tbl SELECT 100 FROM generate_series(1, 60);
+step i_detach:
+ SELECT injection_points_detach('nbtree-walk-left');
+ SELECT injection_points_wakeup('nbtree-walk-left');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_wakeup
+-----------------------
+
+(1 row)
+
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-step-right
+step b_scan: <... completed>
+col
+---
+601
+501
+401
+301
+201
+101
+ 1
+(7 rows)
+
+step b_detach:
+ SELECT injection_points_detach('nbtree-walk-left-step-right');
+ SELECT injection_points_detach('nbtree-walk-left-restart');
+ SELECT injection_points_detach('nbtree-walk-left-deleted');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+
+starting permutation: b_attach b_scan i_insert 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');
+ SELECT injection_points_attach('nbtree-walk-left-restart', 'notice');
+ SELECT injection_points_attach('nbtree-walk-left-deleted', 'notice');
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+step b_scan: SELECT col FROM backwards_scan_tbl
+ WHERE col % 100 = 1 AND pg_backend_pid() <> 0
+ 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('nbtree-walk-left');
+ SELECT injection_points_wakeup('nbtree-walk-left');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_wakeup
+-----------------------
+
+(1 row)
+
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-step-right
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-step-right
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-step-right
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-step-right
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-restart
+step b_scan: <... completed>
+col
+---
+601
+501
+401
+301
+201
+101
+ 1
+(7 rows)
+
+step b_detach:
+ SELECT injection_points_detach('nbtree-walk-left-step-right');
+ SELECT injection_points_detach('nbtree-walk-left-restart');
+ SELECT injection_points_detach('nbtree-walk-left-deleted');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+
+starting permutation: b_attach d_delete_left 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');
+ SELECT injection_points_attach('nbtree-walk-left-restart', 'notice');
+ SELECT injection_points_attach('nbtree-walk-left-deleted', 'notice');
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+step d_delete_left: DELETE FROM backwards_scan_tbl WHERE col < 601;
+step b_scan: SELECT col FROM backwards_scan_tbl
+ WHERE col % 100 = 1 AND pg_backend_pid() <> 0
+ ORDER BY col DESC; <waiting ...>
+step vacuum_tbl: VACUUM backwards_scan_tbl;
+step i_detach:
+ SELECT injection_points_detach('nbtree-walk-left');
+ SELECT injection_points_wakeup('nbtree-walk-left');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_wakeup
+-----------------------
+
+(1 row)
+
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-step-right
+step b_scan: <... completed>
+col
+---
+601
+(1 row)
+
+step b_detach:
+ SELECT injection_points_detach('nbtree-walk-left-step-right');
+ SELECT injection_points_detach('nbtree-walk-left-restart');
+ SELECT injection_points_detach('nbtree-walk-left-deleted');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+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
+step b_attach_nosr:
+ SELECT injection_points_attach('nbtree-walk-left', 'wait');
+ SELECT injection_points_attach('nbtree-walk-left-restart', 'notice');
+ SELECT injection_points_attach('nbtree-walk-left-deleted', 'notice');
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+injection_points_attach
+-----------------------
+
+(1 row)
+
+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 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 ...>
+step vacuum_tbl: VACUUM backwards_scan_tbl;
+step i_detach:
+ SELECT injection_points_detach('nbtree-walk-left');
+ SELECT injection_points_wakeup('nbtree-walk-left');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_wakeup
+-----------------------
+
+(1 row)
+
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-deleted
+scan_session: NOTICE: notice triggered for injection point nbtree-walk-left-restart
+step b_scan_999: <... completed>
+col
+---
+301
+201
+101
+ 1
+(4 rows)
+
+step b_detach_nosr:
+ SELECT injection_points_detach('nbtree-walk-left-restart');
+ SELECT injection_points_detach('nbtree-walk-left-deleted');
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
+injection_points_detach
+-----------------------
+
+(1 row)
+
},
'isolation': {
'specs': [
+ 'backwards-scan-concurrent-splits',
'predicate-empty-index',
],
+ 'runningcheck': false, # see syscache-update-pruned
},
}
--- /dev/null
+# Backwards scan isolation test
+#
+# Backwards scans cannot unreservedly trust their saved left link: by the time
+# the scan follows it, concurrent page splits and/or page deletions may have
+# left it pointing to a page that is no longer the correct page for the scan
+# to read next. The scan checks for this by verifying that the pointed-to
+# page's right link still points back to the page that the scan just read, and
+# recovers when it doesn't (see nbtree/README for details).
+#
+# Each permutation makes the scan wait "between pages" at the nbtree-walk-left
+# injection point while the concurrent session splits and/or deletes pages,
+# then wakes it, forcing the scan to take one of its recovery paths. The
+# notice-mode injection points confirm which recovery steps ran.
+#
+# Note: the permutations' expected notifications (and the leaf pages that each
+# concurrent session step splits or deletes) assume the default 8KB BLCKSZ.
+
+setup
+{
+ CREATE EXTENSION injection_points;
+ 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;
+}
+setup
+{
+ VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) backwards_scan_tbl;
+}
+teardown
+{
+ DROP EXTENSION injection_points;
+ DROP TABLE backwards_scan_tbl;
+}
+
+session scan_session
+setup {
+ SELECT injection_points_set_local();
+ SET enable_seqscan=off;
+ SET enable_sort=off;
+}
+step b_attach {
+ SELECT injection_points_attach('nbtree-walk-left', 'wait');
+ SELECT injection_points_attach('nbtree-walk-left-step-right', 'notice');
+ SELECT injection_points_attach('nbtree-walk-left-restart', 'notice');
+ SELECT injection_points_attach('nbtree-walk-left-deleted', 'notice');
+}
+# Variant that doesn't attach to nbtree-walk-left-step-right, for
+# permutations whose number of step right attempts varies with the amount of
+# free space that index tuples' varying alignment padding leaves on each page
+step b_attach_nosr {
+ SELECT injection_points_attach('nbtree-walk-left', 'wait');
+ SELECT injection_points_attach('nbtree-walk-left-restart', 'notice');
+ SELECT injection_points_attach('nbtree-walk-left-deleted', 'notice');
+}
+# Note: Both scan variants call parallel restricted pg_backend_pid() so that
+# the scan runs in the leader process under debug_parallel_query
+step b_scan { SELECT col FROM backwards_scan_tbl
+ WHERE col % 100 = 1 AND pg_backend_pid() <> 0
+ ORDER BY col DESC; }
+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; }
+step b_detach {
+ SELECT injection_points_detach('nbtree-walk-left-step-right');
+ SELECT injection_points_detach('nbtree-walk-left-restart');
+ SELECT injection_points_detach('nbtree-walk-left-deleted');
+}
+step b_detach_nosr {
+ SELECT injection_points_detach('nbtree-walk-left-restart');
+ SELECT injection_points_detach('nbtree-walk-left-deleted');
+}
+
+session concurrent_session
+step i_insert { INSERT INTO backwards_scan_tbl SELECT i FROM generate_series(-2000, 700) i; }
+step i_insert_dups { INSERT INTO backwards_scan_tbl SELECT 100 FROM generate_series(1, 60); }
+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 vacuum_tbl { VACUUM backwards_scan_tbl; }
+step i_detach {
+ SELECT injection_points_detach('nbtree-walk-left');
+ SELECT injection_points_wakeup('nbtree-walk-left');
+}
+
+# A single concurrent page split. When the backwards scan session wakes up,
+# its search recovers by stepping right just once.
+permutation b_attach
+ b_scan
+ i_insert_dups
+ i_detach
+ b_detach
+
+# Many concurrent page splits. When the backwards scan session wakes up, its
+# search steps right the maximum number of times before giving up and
+# starting over with the right sibling page's current left link.
+permutation b_attach
+ b_scan
+ i_insert
+ i_detach
+ b_detach
+
+# Concurrent deletion of all pages to the left of the page that the scan just
+# read. When the backwards scan session wakes up, its search determines that
+# the scan has no page to the left to move to, ending the scan.
+permutation b_attach
+ d_delete_left
+ b_scan
+ vacuum_tbl
+ i_detach
+ b_detach
+
+# Concurrent deletion of the page that the scan just read (which the scan can
+# only safely rely on when a search locates its left sibling using its saved
+# right link, which the deleted page's right sibling has acquired). The scan
+# just read a page whose tuples all pointed to dead-to-all heap tuples, which
+# VACUUM deletes during the scan's wait, along with all nearby pages.
+permutation b_attach_nosr
+ i_grow
+ d_delete_mid
+ b_scan_999
+ vacuum_tbl
+ i_detach
+ b_detach_nosr