From adbf7b9897601fea2ebc77c2fd56f07b2590ae56 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Wed, 2 Sep 2026 21:59:36 +0000 Subject: [PATCH v3 3/3] 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 Reviewed-by: Palak Chaturvedi 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 | 9 ++ contrib/pg_prewarm/meson.build | 4 + .../t/002_autoprewarm_lock_yield.pl | 105 ++++++++++++++++++ 4 files changed, 121 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..660c1875298 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,14 @@ apw_prewarm_blocks(Relation rel, struct AutoPrewarmReadStreamData *p) blocks_since_check = 0; + /* + * Pass the relation name so a test can wait here for a specific + * relation only, instead of the first one that reaches this + * point. + */ + INJECTION_POINT("autoprewarm-before-lock-check", + RelationGetRelationName(rel)); + 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..10fb11f4480 --- /dev/null +++ b/contrib/pg_prewarm/t/002_autoprewarm_lock_yield.pl @@ -0,0 +1,105 @@ +# 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 warm_tbl (id int, pad text); + INSERT INTO warm_tbl SELECT g, repeat('x', 500) + FROM generate_series(1, 1000000) g; +)); + +# The table must exceed the worker's lock-check interval (in blocks) so that +# the worker reaches the injection point while still scanning it. +my $nblocks = $node->safe_psql('postgres', + "SELECT pg_relation_size('warm_tbl') / current_setting('block_size')::int"); +ok($nblocks > 32, "table has more than 32 blocks ($nblocks)"); + +# Warm the table and record its blocks so the worker reloads them on restart. +$node->safe_psql('postgres', "SELECT pg_prewarm('warm_tbl', 'buffer')"); +$node->safe_psql('postgres', "SELECT autoprewarm_dump_now()"); + +$node->restart; + +# Pause the worker mid-prewarm, but only while it scans the target table. +# Other relations in the dump (some catalogs have more than 32 blocks) reach +# this point first, so without the condition the worker could stop holding a +# lock on the wrong relation and the TRUNCATE below would not block. +$node->safe_psql('postgres', + "SELECT injection_points_attach('autoprewarm-before-lock-check', 'wait', 'warm_tbl')"); +$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 warm_tbl; +)); +$node->poll_query_until('postgres', q( + SELECT count(*) > 0 FROM pg_stat_activity + WHERE query LIKE '%TRUNCATE warm_tbl%' 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 the table, 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