From 6bdc0246507d0e5688ae8592bd59f69eb61811b3 Mon Sep 17 00:00:00 2001 From: Sami Imseih Date: Fri, 15 May 2026 09:07:40 -0500 Subject: [PATCH v8 2/2] Add injection point test for vacuum skip_locked stats race Add an isolation test exercising the race window between VACUUM (SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops. Two scenarios are tested: 1. Table dropped (committed) while vacuumer is blocked at the injection point: no orphaned stats entry is created. 2. DROP TABLE rolled back while vacuumer is blocked: skip is still recorded since the table and its stats entry survive. --- src/backend/utils/activity/pgstat_relation.c | 2 + .../expected/vacuum_skip_lock_stats.out | 91 +++++++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/vacuum_skip_lock_stats.spec | 67 ++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c index f47c2958c1f..be6a6091460 100644 --- a/src/backend/utils/activity/pgstat_relation.c +++ b/src/backend/utils/activity/pgstat_relation.c @@ -21,6 +21,7 @@ #include "access/twophase_rmgr.h" #include "access/xact.h" #include "catalog/catalog.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/pgstat_internal.h" @@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags) return; /* somebody deleted the rel, forget it */ isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared; ReleaseSysCache(classTup); + INJECTION_POINT("expand-vacuum-rel-skip-locked", NULL); /* Store the data in the table's hash table entry. */ ts = GetCurrentTimestamp(); diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out new file mode 100644 index 00000000000..84c2180cc80 --- /dev/null +++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out @@ -0,0 +1,91 @@ +Parsed test spec with 3 sessions + +starting permutation: lock vacuum unlock drop_table wakeup check_stats detach +injection_points_attach +----------------------- + +(1 row) + +step lock: + BEGIN; + LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE; + +s2: WARNING: skipping vacuum of "test_skip" --- lock not available +step vacuum: VACUUM (SKIP_LOCKED) test_skip; +step unlock: COMMIT; +step drop_table: DROP TABLE test_skip; +step wakeup: SELECT injection_points_wakeup('expand-vacuum-rel-skip-locked'); +injection_points_wakeup +----------------------- + +(1 row) + +step vacuum: <... completed> +step check_stats: + SELECT pg_stat_force_next_flush(); + SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count + FROM saved_oid; + +pg_stat_force_next_flush +------------------------ + +(1 row) + +skip_count +---------- + 0 +(1 row) + +step detach: SELECT injection_points_detach('expand-vacuum-rel-skip-locked'); +injection_points_detach +----------------------- + +(1 row) + + +starting permutation: lock vacuum unlock rollback_drop wakeup check_stats detach +injection_points_attach +----------------------- + +(1 row) + +step lock: + BEGIN; + LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE; + +s2: WARNING: skipping vacuum of "test_skip" --- lock not available +step vacuum: VACUUM (SKIP_LOCKED) test_skip; +step unlock: COMMIT; +step rollback_drop: + BEGIN; + DROP TABLE test_skip; + ROLLBACK; + +step wakeup: SELECT injection_points_wakeup('expand-vacuum-rel-skip-locked'); +injection_points_wakeup +----------------------- + +(1 row) + +step vacuum: <... completed> +step check_stats: + SELECT pg_stat_force_next_flush(); + SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count + FROM saved_oid; + +pg_stat_force_next_flush +------------------------ + +(1 row) + +skip_count +---------- + 1 +(1 row) + +step detach: SELECT injection_points_detach('expand-vacuum-rel-skip-locked'); +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 59dba1cb023..48cacfb81a8 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -51,6 +51,7 @@ tests += { 'repack_toast', 'syscache-update-pruned', 'heap_lock_update', + 'vacuum_skip_lock_stats', ], 'runningcheck': false, # see syscache-update-pruned # Some tests wait for all snapshots, so avoid parallel execution diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec new file mode 100644 index 00000000000..c37294a3ba1 --- /dev/null +++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec @@ -0,0 +1,67 @@ +# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting +# and concurrent DROP TABLE. +# +# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped +# statistics via pgstat_report_skipped_vacuum_analyze(). An injection +# point after the syscache lookup but before the stats update allows us +# to verify that a concurrent DROP does not leave orphaned stats entries. + +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE test_skip (id int); + INSERT INTO test_skip VALUES (1); + ANALYZE test_skip; + SELECT pg_stat_force_next_flush(); + CREATE TABLE saved_oid (oid_val oid); + INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip'; +} + +teardown +{ + DROP TABLE IF EXISTS test_skip; + DROP TABLE IF EXISTS saved_oid; + DROP EXTENSION injection_points; +} + +# s1: holds the lock so VACUUM skips the table +session s1 +step lock +{ + BEGIN; + LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE; +} +step unlock { COMMIT; } + +# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip +session s2 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('expand-vacuum-rel-skip-locked', 'wait'); +} +step vacuum { VACUUM (SKIP_LOCKED) test_skip; } +step detach { SELECT injection_points_detach('expand-vacuum-rel-skip-locked'); } + +# s3: drops table or wakes up the vacuumer +session s3 +step drop_table { DROP TABLE test_skip; } +step rollback_drop +{ + BEGIN; + DROP TABLE test_skip; + ROLLBACK; +} +step wakeup { SELECT injection_points_wakeup('expand-vacuum-rel-skip-locked'); } +step check_stats +{ + SELECT pg_stat_force_next_flush(); + SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count + FROM saved_oid; +} + +# Table dropped while vacuumer is blocked: no orphaned stats entry. +permutation lock vacuum(wakeup) unlock drop_table wakeup check_stats detach + +# DROP rolled back while vacuumer is blocked: skip is still recorded. +permutation lock vacuum(wakeup) unlock rollback_drop wakeup check_stats detach -- 2.50.1 (Apple Git-155)