Hi, On 2024-12-02 11:31:48 -0500, Andres Freund wrote: > I think it'd be good if we added a test that shows the failure mechanism so > that we don't re-introduce this in the future. Evidently this failure isn't > immediately obvious...
Attached is an isolationtest that reliably shows wrong query results. Greetings, Andres Freund
>From a666e6da7af9a0af31ae506de8c2c229b713f8a6 Mon Sep 17 00:00:00 2001 From: Andres Freund <and...@anarazel.de> Date: Mon, 2 Dec 2024 14:38:11 -0500 Subject: [PATCH v1] isolationtester showing broken index-only bitmap heap scan --- .../expected/index-only-bitmapscan.out | 28 ++++++ src/test/isolation/isolation_schedule | 1 + .../specs/index-only-bitmapscan.spec | 85 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 src/test/isolation/expected/index-only-bitmapscan.out create mode 100644 src/test/isolation/specs/index-only-bitmapscan.spec diff --git a/src/test/isolation/expected/index-only-bitmapscan.out b/src/test/isolation/expected/index-only-bitmapscan.out new file mode 100644 index 00000000000..132ff1bda70 --- /dev/null +++ b/src/test/isolation/expected/index-only-bitmapscan.out @@ -0,0 +1,28 @@ +Parsed test spec with 2 sessions + +starting permutation: s2_vacuum s2_mod s1_begin s1_prepare s1_fetch_1 s2_vacuum s1_fetch_all s1_commit +step s2_vacuum: VACUUM (TRUNCATE false) ios_bitmap; +step s2_mod: + DELETE FROM ios_bitmap WHERE a > 1; + +step s1_begin: BEGIN; +step s1_prepare: + DECLARE foo NO SCROLL CURSOR FOR SELECT row_number() OVER () FROM ios_bitmap WHERE a > 0 or b > 0; + +step s1_fetch_1: + FETCH FROM foo; + +row_number +---------- + 1 +(1 row) + +step s2_vacuum: VACUUM (TRUNCATE false) ios_bitmap; +step s1_fetch_all: + FETCH ALL FROM foo; + +row_number +---------- +(0 rows) + +step s1_commit: COMMIT; diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 143109aa4da..e3c669a29c7 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -17,6 +17,7 @@ test: partial-index test: two-ids test: multiple-row-versions test: index-only-scan +test: index-only-bitmapscan test: predicate-lock-hot-tuple test: update-conflict-out test: deadlock-simple diff --git a/src/test/isolation/specs/index-only-bitmapscan.spec b/src/test/isolation/specs/index-only-bitmapscan.spec new file mode 100644 index 00000000000..9962b8dc531 --- /dev/null +++ b/src/test/isolation/specs/index-only-bitmapscan.spec @@ -0,0 +1,85 @@ +# index-only-bitmapscan test showing wrong results +# +setup +{ + -- by using a low fillfactor and a wide tuple we can get multiple blocks + -- with just few rows + CREATE TABLE ios_bitmap (a int NOT NULL, b int not null, pad char(1024) default '') + WITH (AUTOVACUUM_ENABLED = false, FILLFACTOR = 10); + + INSERT INTO ios_bitmap SELECT g.i, g.i FROM generate_series(1, 10) g(i); + + CREATE INDEX ios_bitmap_a ON ios_bitmap(a); + CREATE INDEX ios_bitmap_b ON ios_bitmap(b); +} + +teardown +{ + DROP TABLE ios_bitmap; +} + + +session s1 + +setup { + SET enable_seqscan = false; +} + +step s1_begin { BEGIN; } +step s1_commit { COMMIT; } + + +# The test query uses an or between two indexes to ensure make it more likely +# to use a bitmap index scan +# +# The row_number() hack is a way to have something returned (isolationtester +# doesn't display empty rows) while still allowing for the index-only scan +# optimization in bitmap heap scans, which requires an empty targetlist. +step s1_prepare { + DECLARE foo NO SCROLL CURSOR FOR SELECT row_number() OVER () FROM ios_bitmap WHERE a > 0 or b > 0; +} + +step s1_fetch_1 { + FETCH FROM foo; +} + +step s1_fetch_all { + FETCH ALL FROM foo; +} + + +session s2 + +# Don't delete row 1 so we have a row for the cursor to "rest" on. +step s2_mod +{ + DELETE FROM ios_bitmap WHERE a > 1; +} + +# Disable truncation, as otherwise we'll just wait for a timeout while trying +# to acquire the lock +step s2_vacuum { VACUUM (TRUNCATE false) ios_bitmap; } + +permutation + # Vacuum first, to ensure VM exists, otherwise the bitmapscan will consider + # VM to be size 0, due to caching. Can't do that in setup because + s2_vacuum + + # delete nearly all rows, to make issue visible + s2_mod + # create a cursor + s1_begin + s1_prepare + + # fetch one row from the cursor, that ensures the index scan portion is done + # before the vacuum in the next step + s1_fetch_1 + + # with the bug this vacuum will mark pages as all-visible that the scan in + # the next step then considers all-visible, despite all rows from those + # pages having been removed. + s2_vacuum + # if this returns any rows, we're busted + s1_fetch_all + + s1_commit -- 2.45.2.746.g06e570c0df.dirty