From c99597016af620c6f527e06b445bed3c04158952 Mon Sep 17 00:00:00 2001 From: Mikhail Nikalayeu Date: Sun, 8 Mar 2026 19:20:02 +0100 Subject: [PATCH v2] [PATCH v2] Add test to demonstrate bug in SSI --- src/backend/storage/lmgr/predicate.c | 9 ++ src/test/modules/injection_points/Makefile | 1 + .../expected/predicate-lock-page-split.out | 105 ++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/predicate-lock-page-split.spec | 128 ++++++++++++++++++ 5 files changed, 244 insertions(+) create mode 100644 src/test/modules/injection_points/expected/predicate-lock-page-split.out create mode 100644 src/test/modules/injection_points/specs/predicate-lock-page-split.spec diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c index 548b4f66470..7eef2ca0418 100644 --- a/src/backend/storage/lmgr/predicate.c +++ b/src/backend/storage/lmgr/predicate.c @@ -212,6 +212,7 @@ #include "storage/proc.h" #include "storage/procarray.h" #include "utils/guc_hooks.h" +#include "utils/injection_point.h" #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/wait_event.h" @@ -3157,6 +3158,7 @@ PredicateLockPageSplit(Relation relation, BlockNumber oldblkno, * memory barrier in the LWLock acquisition guarantees that this read * occurs while the buffer page lock is held. */ + INJECTION_POINT("predicate-lock-page-split", NULL); if (!TransactionIdIsValid(PredXact->SxactGlobalXmin)) return; @@ -3256,6 +3258,10 @@ SetNewSxactGlobalXmin(void) PredXact->SxactGlobalXmin = InvalidTransactionId; PredXact->SxactGlobalXminCount = 0; +#ifdef USE_INJECTION_POINTS + INJECTION_POINT_CACHED("predicate-set-sxact-global-xmin-invalid", NULL); +#endif + dlist_foreach(iter, &PredXact->activeList) { SERIALIZABLEXACT *sxact = @@ -3369,6 +3375,9 @@ ReleasePredicateLocks(bool isCommit, bool isReadOnlySafe) Assert(LocalPredicateLockHash == NULL); return; } +#ifdef USE_INJECTION_POINTS + INJECTION_POINT_LOAD("predicate-set-sxact-global-xmin-invalid"); +#endif LWLockAcquire(SerializableXactHashLock, LW_EXCLUSIVE); diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index a41d781f8c9..73ed09eaa22 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -14,6 +14,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ + predicate-lock-page-split \ syscache-update-pruned \ heap_lock_update diff --git a/src/test/modules/injection_points/expected/predicate-lock-page-split.out b/src/test/modules/injection_points/expected/predicate-lock-page-split.out new file mode 100644 index 00000000000..deeced73d88 --- /dev/null +++ b/src/test/modules/injection_points/expected/predicate-lock-page-split.out @@ -0,0 +1,105 @@ +Parsed test spec with 5 sessions + +starting permutation: s1_begin bump_xmin s2_begin s3_begin s1_insert s2_insert_wait_at_page_split s1_commit_wait_in_SetNewSxactGlobalXmin wakeup_s2_then_s1 s3_insert s3_commit s2_commit verify +injection_points_attach +----------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s1_begin: + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) > 0 AS locked FROM test_table; + +locked +------ +t +(1 row) + +step bump_xmin: + DO $$ + BEGIN + PERFORM pg_current_xact_id(); + END; + $$; + +step s2_begin: + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) > 0 AS locked FROM test_table; + +locked +------ +t +(1 row) + +step s3_begin: + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) > 0 AS locked FROM test_table; + +locked +------ +t +(1 row) + +step s1_insert: + INSERT INTO test_table + SELECT max(id) + 1 + FROM test_table; + +step s2_insert_wait_at_page_split: + DO $$ + DECLARE + next_id int := (SELECT max(id) + 2 FROM test_table); + base_size bigint := pg_relation_size('test_table_id_idx'); + BEGIN + LOOP + INSERT INTO test_table VALUES (next_id); + EXIT WHEN pg_relation_size('test_table_id_idx') > base_size; + next_id := next_id + 1; + END LOOP; + END; + $$; + +step s1_commit_wait_in_SetNewSxactGlobalXmin: + COMMIT; + +step wakeup_s2_then_s1: + SELECT injection_points_wakeup('predicate-lock-page-split'); + SELECT injection_points_wakeup('predicate-set-sxact-global-xmin-invalid'); + +step s2_insert_wait_at_page_split: <... completed> +step s1_commit_wait_in_SetNewSxactGlobalXmin: <... completed> +step s3_insert: + INSERT INTO test_table VALUES (1000000); + +ERROR: could not serialize access due to read/write dependencies among transactions +step wakeup_s2_then_s1: <... completed> +injection_points_wakeup +----------------------- + +(1 row) + +injection_points_wakeup +----------------------- + +(1 row) + +step s3_commit: + COMMIT; + +step s2_commit: + COMMIT; + +ERROR: could not serialize access due to read/write dependencies among transactions +step verify: + SELECT EXISTS (SELECT 1 FROM test_table WHERE id = 1000000) AS inserted; + +inserted +-------- +f +(1 row) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index fcc85414515..6ff7005a9bf 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -45,6 +45,7 @@ tests += { 'specs': [ 'basic', 'inplace', + 'predicate-lock-page-split', 'syscache-update-pruned', 'heap_lock_update', ], diff --git a/src/test/modules/injection_points/specs/predicate-lock-page-split.spec b/src/test/modules/injection_points/specs/predicate-lock-page-split.spec new file mode 100644 index 00000000000..bfd7ecc85c6 --- /dev/null +++ b/src/test/modules/injection_points/specs/predicate-lock-page-split.spec @@ -0,0 +1,128 @@ +# Test for race condition in PredicateLockPageSplit +# +# When SetNewSxactGlobalXmin() temporarily sets SxactGlobalXmin to +# InvalidTransactionId, a concurrent PredicateLockPageSplit() can see +# the invalid value and skip transferring SIREAD locks to the new page. +# This lets a third transaction insert onto the new page without SSI +# detecting the rw-conflict. +# +setup +{ + CREATE EXTENSION IF NOT EXISTS injection_points; + DROP TABLE IF EXISTS test_table; + + CREATE TABLE test_table (id int); + CREATE INDEX test_table_id_idx ON test_table USING btree (id); + INSERT INTO test_table VALUES (1); +} + +teardown +{ + DROP TABLE IF EXISTS test_table; + DROP EXTENSION IF EXISTS injection_points; +} + +session s0 +step bump_xmin { + DO $$ + BEGIN + PERFORM pg_current_xact_id(); + END; + $$; +} +step verify { + SELECT EXISTS (SELECT 1 FROM test_table WHERE id = 1000000) AS inserted; +} + +session s1 +setup { + SELECT injection_points_set_local(); + SELECT injection_points_attach('predicate-set-sxact-global-xmin-invalid', 'wait'); +} +step s1_begin { + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) > 0 AS locked FROM test_table; +} +step s1_insert { + INSERT INTO test_table + SELECT max(id) + 1 + FROM test_table; +} +step s1_commit_wait_in_SetNewSxactGlobalXmin { + COMMIT; +} + +session s2 +setup { + SELECT injection_points_set_local(); + SELECT injection_points_attach('predicate-lock-page-split', 'wait'); +} +step s2_begin { + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) > 0 AS locked FROM test_table; +} +step s2_insert_wait_at_page_split { + DO $$ + DECLARE + next_id int := (SELECT max(id) + 2 FROM test_table); + base_size bigint := pg_relation_size('test_table_id_idx'); + BEGIN + LOOP + INSERT INTO test_table VALUES (next_id); + EXIT WHEN pg_relation_size('test_table_id_idx') > base_size; + next_id := next_id + 1; + END LOOP; + END; + $$; +} +step s2_commit { + COMMIT; +} + +session s3 +step s3_begin { + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) > 0 AS locked FROM test_table; +} +step s3_insert { + INSERT INTO test_table VALUES (1000000); +} +step s3_commit { + COMMIT; +} + +session s4 +step wakeup_s2_then_s1 { + SELECT injection_points_wakeup('predicate-lock-page-split'); + SELECT injection_points_wakeup('predicate-set-sxact-global-xmin-invalid'); +} + +# s1_begin: s1 reads from the table, establishing SIREAD locks on the index +# bump_xmin: advance xmin so s2/s3 get a higher xmin than s1 +# s2_begin, s3_begin: s2 and s3 read from the table (same snapshot as s1) +# +# s1_insert: s1 inserts max(id)+1 +# s2_insert_wait_at_page_split: s2 inserts ascending values until a real +# btree page split happens, then waits in PredicateLockPageSplit before +# checking SxactGlobalXmin +# s1_commit_wait_in_SetNewSxactGlobalXmin: after s2 is already waiting, +# s1 commits and waits after SetNewSxactGlobalXmin sets SxactGlobalXmin +# to InvalidTransactionId +# wakeup_s2_then_s1: wake s2 (sees InvalidTransactionId, skips SIREAD +# lock transfer), then wake s1 +# s3_insert: s3 inserts 1000000 onto the new page (no SIREAD locks = bug) +# s3_commit: s3 commits (should have been aborted by SSI) +# s2_commit: s2 aborts due to serialization failure +permutation + s1_begin + bump_xmin + s2_begin + s3_begin + s1_insert + s2_insert_wait_at_page_split + s1_commit_wait_in_SetNewSxactGlobalXmin + wakeup_s2_then_s1(s2_insert_wait_at_page_split,s1_commit_wait_in_SetNewSxactGlobalXmin) + s3_insert + s3_commit + s2_commit + verify -- 2.43.0