From: Sami Imseih Date: Fri, 24 Jul 2026 22:45:53 +0900 Subject: [PATCH v14 2/2] Add injection point test for vacuum skip_locked stats 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 + src/test/modules/injection_points/Makefile | 3 +- .../expected/vacuum_skip_lock_stats.out | 93 +++++++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/vacuum_skip_lock_stats.spec | 68 ++++++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) 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 d8259823925..6c1e2a6c0f2 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("skipped-vacuum-analyze-before-entry-lock", NULL); /* Store the data in the table's hash table entry. */ ts = GetCurrentTimestamp(); diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index fac80f3a4a7..b833d9d481d 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -20,7 +20,8 @@ ISOLATION = basic \ repack_toast \ syscache-update-pruned \ wait_cleanup \ - heap_lock_update + heap_lock_update \ + vacuum_skip_lock_stats # some isolation tests require wal_level=replica ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf 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..0efed3db2db --- /dev/null +++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out @@ -0,0 +1,93 @@ +Parsed test spec with 3 sessions + +starting permutation: lock vacuum unlock drop_table wakeup noop 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('skipped-vacuum-analyze-before-entry-lock'); +injection_points_wakeup +----------------------- + +(1 row) + +step vacuum: <... completed> +step noop: +step check_stats: + SELECT pg_stat_force_next_flush(); + SELECT pg_stat_get_lock_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('skipped-vacuum-analyze-before-entry-lock'); +injection_points_detach +----------------------- + +(1 row) + + +starting permutation: lock vacuum unlock rollback_drop wakeup noop 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('skipped-vacuum-analyze-before-entry-lock'); +injection_points_wakeup +----------------------- + +(1 row) + +step vacuum: <... completed> +step noop: +step check_stats: + SELECT pg_stat_force_next_flush(); + SELECT pg_stat_get_lock_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('skipped-vacuum-analyze-before-entry-lock'); +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 163b6374ebc..a96753fc326 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -52,6 +52,7 @@ tests += { 'syscache-update-pruned', 'wait_cleanup', '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..b277b1e80f7 --- /dev/null +++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec @@ -0,0 +1,68 @@ +# 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('skipped-vacuum-analyze-before-entry-lock', 'wait'); +} +step vacuum { VACUUM (SKIP_LOCKED) test_skip; } +step noop {} +step detach { SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); } + +# 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('skipped-vacuum-analyze-before-entry-lock'); } +step check_stats +{ + SELECT pg_stat_force_next_flush(); + SELECT pg_stat_get_lock_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 noop check_stats detach + +# DROP rolled back while vacuumer is blocked: skip is still recorded. +permutation lock vacuum(wakeup) unlock rollback_drop wakeup noop check_stats detach -- 2.43.0 --Multipart=_Tue__28_Jul_2026_23_27_10_+0900_5Qx+OCS2/NmQO9oH Content-Type: text/x-diff; name="v14-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch" Content-Disposition: attachment; filename="v14-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch" Content-Transfer-Encoding: 7bit