From e007ebf9e56f3bb324dd46469587dc74335fe093 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Sun, 2 Aug 2026 03:57:56 +0000
Subject: [PATCH v2 2/2] Add test for autoprewarm yielding to conflicting lock
 requests.

Add an injection point in the autoprewarm worker's block-read
loop and a TAP test that uses it. The test pauses the worker
mid-prewarm, starts a TRUNCATE that blocks on the conflicting
lock, then resumes the worker and checks that it releases its
lock, lets the TRUNCATE finish, and gives up the relation
(reporting fewer prewarmed blocks than it dumped).

The worker only loads the dump at startup and injection points do
not survive a restart, so the wait can only be armed after the
restart. A large table and buffer pool keep the scan running long
enough to attach the injection point and still catch a later
check. That fixed size makes the test too heavy for the
buildfarm; it is meant to be run locally.

Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/CAHg%2BQDfdoR%3D7iqEAvLW9qtzV0Sx1wp2FuALeamqcCdiVEmMF-Q%40mail.gmail.com
---
 contrib/pg_prewarm/Makefile                   |   3 +
 contrib/pg_prewarm/autoprewarm.c              |   3 +
 contrib/pg_prewarm/meson.build                |   4 +
 .../t/002_autoprewarm_lock_yield.pl           | 101 ++++++++++++++++++
 4 files changed, 111 insertions(+)
 create mode 100644 contrib/pg_prewarm/t/002_autoprewarm_lock_yield.pl

diff --git a/contrib/pg_prewarm/Makefile b/contrib/pg_prewarm/Makefile
index 617ac8e09b2..53bce44971a 100644
--- a/contrib/pg_prewarm/Makefile
+++ b/contrib/pg_prewarm/Makefile
@@ -12,6 +12,9 @@ PGFILEDESC = "pg_prewarm - preload relation data into system buffer cache"
 
 REGRESS = pg_prewarm
 
+EXTRA_INSTALL = src/test/modules/injection_points
+export enable_injection_points
+
 TAP_TESTS = 1
 
 ifdef USE_PGXS
diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index 0e909e34be2..4a9f182a6e7 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -47,6 +47,7 @@
 #include "storage/smgr.h"
 #include "tcop/tcopprot.h"
 #include "utils/guc.h"
+#include "utils/injection_point.h"
 #include "utils/rel.h"
 #include "utils/relfilenumbermap.h"
 #include "utils/timestamp.h"
@@ -549,6 +550,8 @@ apw_prewarm_blocks(Relation rel, struct AutoPrewarmReadStreamData *p)
 
 			blocks_since_check = 0;
 
+			INJECTION_POINT("autoprewarm-before-lock-check", NULL);
+
 			INSTR_TIME_SET_CURRENT(currenttime);
 			elapsed = currenttime;
 			INSTR_TIME_SUBTRACT(elapsed, starttime);
diff --git a/contrib/pg_prewarm/meson.build b/contrib/pg_prewarm/meson.build
index e70546a451b..e43b9b2e1b8 100644
--- a/contrib/pg_prewarm/meson.build
+++ b/contrib/pg_prewarm/meson.build
@@ -35,8 +35,12 @@ tests += {
     ],
   },
   'tap': {
+    'env': {
+      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_basic.pl',
+      't/002_autoprewarm_lock_yield.pl',
     ],
   },
 }
diff --git a/contrib/pg_prewarm/t/002_autoprewarm_lock_yield.pl b/contrib/pg_prewarm/t/002_autoprewarm_lock_yield.pl
new file mode 100644
index 00000000000..f66de6daf17
--- /dev/null
+++ b/contrib/pg_prewarm/t/002_autoprewarm_lock_yield.pl
@@ -0,0 +1,101 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test that the autoprewarm worker gives up a relation when a conflicting
+# lock request is waiting, letting the DDL proceed.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# The worker only loads the dump at startup and injection points do not
+# survive a restart, so the wait can only be armed after the restart below.
+# A large table and buffer pool keep the scan running long enough to attach
+# the injection point and still catch a later check. That size makes this a
+# heavy, manual/local test rather than one for the buildfarm.
+$node->append_conf(
+	'postgresql.conf', qq{
+shared_preload_libraries = 'pg_prewarm,injection_points'
+pg_prewarm.autoprewarm = true
+pg_prewarm.autoprewarm_interval = 0
+autovacuum = off
+shared_buffers = '2GB'
+});
+$node->start;
+
+# The injection_points extension may not be installed under installcheck.
+if (!$node->check_extension('injection_points'))
+{
+	plan skip_all => 'Extension injection_points not installed';
+}
+
+$node->safe_psql('postgres', q(
+	CREATE EXTENSION pg_prewarm;
+	CREATE EXTENSION injection_points;
+));
+
+$node->safe_psql('postgres', q(
+	CREATE TABLE t (id int, pad text);
+	INSERT INTO t SELECT g, repeat('x', 500)
+		FROM generate_series(1, 1000000) g;
+));
+
+my $nblocks = $node->safe_psql('postgres',
+	"SELECT pg_relation_size('t') / current_setting('block_size')::int");
+ok($nblocks > 32, "table t has more than 32 blocks ($nblocks)");
+
+# Warm t and record its blocks so the worker reloads them after a restart.
+$node->safe_psql('postgres', "SELECT pg_prewarm('t', 'buffer')");
+my $dumped = $node->safe_psql('postgres', "SELECT autoprewarm_dump_now()");
+ok($dumped > 32, "autoprewarm dumped more than 32 blocks ($dumped)");
+
+$node->restart;
+
+# Pause the worker mid-prewarm.
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('autoprewarm-before-lock-check', 'wait')");
+$node->wait_for_event('autoprewarm worker', 'autoprewarm-before-lock-check');
+
+# TRUNCATE now blocks on the AccessExclusiveLock the worker conflicts with.
+my $truncate = $node->background_psql('postgres');
+$truncate->query_until(qr/starting_truncate/, q(
+	\echo starting_truncate
+	TRUNCATE t;
+));
+$node->poll_query_until('postgres', q(
+	SELECT count(*) > 0 FROM pg_stat_activity
+	WHERE query LIKE '%TRUNCATE t%' AND wait_event_type = 'Lock';
+)) or die "timed out waiting for TRUNCATE to block on the lock";
+
+# Resume the worker; it should see the waiter and release its lock.
+my $log_offset = -s $node->logfile;
+$node->safe_psql('postgres',
+	"SELECT injection_points_detach('autoprewarm-before-lock-check')");
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('autoprewarm-before-lock-check')");
+
+$truncate->quit;
+pass('TRUNCATE completed while autoprewarm worker was prewarming');
+
+# Having given up t, the worker warmed fewer blocks than it dumped.
+$node->wait_for_log(
+	qr/autoprewarm successfully prewarmed \d+ of \d+ previously-loaded blocks/,
+	$log_offset);
+my $summary = slurp_file($node->logfile, $log_offset);
+my ($prewarmed, $total) = $summary =~
+	/successfully prewarmed (\d+) of (\d+) previously-loaded blocks/;
+cmp_ok($prewarmed, '<', $total,
+	"worker gave up early: prewarmed $prewarmed of $total blocks");
+
+$node->stop;
+done_testing();
-- 
2.47.3

