v9-0002-Add-injection-point-test-for-vacuum-skip_locked-s.patch
application/octet-stream
Filename: v9-0002-Add-injection-point-test-for-vacuum-skip_locked-s.patch
Type: application/octet-stream
Part: 1
From b555f0e6d91f67fe59a300d1ec7f3f1d74fd760f Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 15 May 2026 09:07:40 -0500
Subject: [PATCH v9 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 +
.../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 be9611b829b..3f9faf4c78b 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/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..0bb44b43632
--- /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; <waiting ...>
+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 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('skipped-vacuum-analyze-before-entry-lock');
+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; <waiting ...>
+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 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('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 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..de81e4c67c4
--- /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('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum { VACUUM (SKIP_LOCKED) test_skip; }
+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_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)