From 614d8bd5574f07db1f6248463178fb0450634cec Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Wed, 25 Feb 2026 15:16:06 +0000 Subject: [PATCH 1/9] pg_buffercache tap test for resize --- contrib/pg_buffercache/Makefile | 4 + contrib/pg_buffercache/meson.build | 10 +- contrib/pg_buffercache/pg_buffercache_pages.c | 68 +++-- contrib/pg_buffercache/t/001_basic.pl | 244 ++++++++++++++++++ 4 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 contrib/pg_buffercache/t/001_basic.pl diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile index 0e618f66aec6e..566cc91118ba4 100644 --- a/contrib/pg_buffercache/Makefile +++ b/contrib/pg_buffercache/Makefile @@ -5,6 +5,9 @@ OBJS = \ $(WIN32RES) \ pg_buffercache_pages.o +EXTRA_INSTALL=src/test/modules/injection_points \ + contrib/test_decoding + EXTENSION = pg_buffercache DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \ pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \ @@ -13,6 +16,7 @@ DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \ PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time" REGRESS = pg_buffercache pg_buffercache_numa +TAP_TESTS = 1 ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build index e681205abb2d8..019ecf091afa7 100644 --- a/contrib/pg_buffercache/meson.build +++ b/contrib/pg_buffercache/meson.build @@ -38,5 +38,13 @@ tests += { 'pg_buffercache', 'pg_buffercache_numa', ], - }, + }, + 'tap': { + 'env': { + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', + }, + 'tests': [ + 't/001_basic.pl', + ], + } } diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index 8a17319ff2a0a..745d07f836875 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -17,6 +17,7 @@ #include "storage/bufmgr.h" #include "utils/rel.h" #include "utils/tuplestore.h" +#include "utils/injection_point.h" #define NUM_BUFFERCACHE_PAGES_MIN_ELEM 8 @@ -199,6 +200,11 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) * snapshot across all buffers, but we do grab the buffer header * locks, so the information of each buffer is self-consistent. */ + + /* + * This point fails when lock bufHdr fails later because of invalid buffer after resize. + */ + INJECTION_POINT("pg-buffercache-scan-start", NULL); for (i = 0; i < currentNBuffers; i++) { BufferDesc *bufHdr; @@ -212,17 +218,27 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) * happen if only we setup the descriptor array large enough at * the server startup time. */ - if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("number of shared buffers changed during scan of buffer cache"))); + // if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) + // ereport(ERROR, + // (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + // errmsg("number of shared buffers changed during scan of buffer cache"))); + elog(DEBUG1, "scanning buffer %d", i); + bufHdr = GetBufferDescriptor(i); - /* Lock each buffer header before inspecting. */ - buf_state = LockBufHdr(bufHdr); - + + /* Injection point during scan to test resize interaction during buffer resize and accessing invalid buffers after resize in case of shrinking */ + if(i==currentNBuffers/2) + INJECTION_POINT("pg-buffercache-after-getdesc", NULL); + /* + * Point of failure is when invalid buffer is accessed after resize. + * All the places where bufHdr is being called. + * One injection point before locking buffer descriptor helps covers all the later cases. + */ + buf_state = LockBufHdr(bufHdr); + elog(DEBUG1, "got buffer descriptor for buffer %d", i); fctx->record[i].bufferid = BufferDescriptorGetBuffer(bufHdr); - fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); + fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); fctx->record[i].reltablespace = bufHdr->tag.spcOid; fctx->record[i].reldatabase = bufHdr->tag.dbOid; fctx->record[i].forknum = BufTagGetForkNum(&bufHdr->tag); @@ -350,6 +366,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) int max_entries; char *startptr, *endptr; + int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); /* If NUMA information is requested, initialize NUMA support. */ if (include_numa && pg_numa_init() == -1) @@ -392,7 +409,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) startptr = (char *) TYPEALIGN_DOWN(os_page_size, BufferGetBlock(1)); endptr = (char *) TYPEALIGN(os_page_size, - (char *) BufferGetBlock(NBuffers) + BLCKSZ); + (char *) BufferGetBlock(currentNBuffers) + BLCKSZ); os_page_count = (endptr - startptr) / os_page_size; /* Used to determine the NUMA node for all OS pages at once */ @@ -418,7 +435,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) Assert(idx == os_page_count); elog(DEBUG1, "NUMA: NBuffers=%d os_page_count=" UINT64_FORMAT " " - "os_page_size=%zu", NBuffers, os_page_count, os_page_size); + "os_page_size=%zu", currentNBuffers, os_page_count, os_page_size); /* * If we ever get 0xff back from kernel inquiry, then we probably @@ -467,7 +484,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) * without reallocating memory. */ pages_per_buffer = Max(1, BLCKSZ / os_page_size) + 1; - max_entries = NBuffers * pages_per_buffer; + max_entries = currentNBuffers * pages_per_buffer; /* Allocate entries for BufferCacheOsPagesRec records. */ fctx->record = (BufferCacheOsPagesRec *) @@ -490,7 +507,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) */ startptr = (char *) TYPEALIGN_DOWN(os_page_size, (char *) BufferGetBlock(1)); idx = 0; - for (i = 0; i < NBuffers; i++) + for (i = 0; i < currentNBuffers; i++) { char *buffptr = (char *) BufferGetBlock(i + 1); BufferDesc *bufHdr; @@ -501,6 +518,11 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) CHECK_FOR_INTERRUPTS(); + if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("number of shared buffers changed during scan of buffer cache"))); + bufHdr = GetBufferDescriptor(i); /* Lock each buffer header before inspecting. */ @@ -632,17 +654,23 @@ pg_buffercache_summary(PG_FUNCTION_ARGS) int32 buffers_dirty = 0; int32 buffers_pinned = 0; int64 usagecount_total = 0; + int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); - for (int i = 0; i < NBuffers; i++) + for (int i = 0; i < currentNBuffers; i++) { BufferDesc *bufHdr; uint64 buf_state; CHECK_FOR_INTERRUPTS(); + if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("number of shared buffers changed during scan of buffer cache"))); + /* * This function summarizes the state of all headers. Locking the * buffer headers wouldn't provide an improved result as the state of @@ -694,10 +722,11 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS) int pinned[BM_MAX_USAGE_COUNT + 1] = {0}; Datum values[NUM_BUFFERCACHE_USAGE_COUNTS_ELEM]; bool nulls[NUM_BUFFERCACHE_USAGE_COUNTS_ELEM] = {0}; + int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); InitMaterializedSRF(fcinfo, 0); - for (int i = 0; i < NBuffers; i++) + for (int i = 0; i < currentNBuffers; i++) { BufferDesc *bufHdr = GetBufferDescriptor(i); uint64 buf_state = pg_atomic_read_u64(&bufHdr->state); @@ -705,6 +734,11 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS) CHECK_FOR_INTERRUPTS(); + if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("number of shared buffers changed during scan of buffer cache"))); + usage_count = BUF_STATE_GET_USAGECOUNT(buf_state); usage_counts[usage_count]++; @@ -755,13 +789,15 @@ pg_buffercache_evict(PG_FUNCTION_ARGS) Buffer buf = PG_GETARG_INT32(0); bool buffer_flushed; + int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); + if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); pg_buffercache_superuser_check("pg_buffercache_evict"); - if (buf < 1 || buf > NBuffers) + if (buf < 1 || buf > currentNBuffers) elog(ERROR, "bad buffer ID: %d", buf); values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, &buffer_flushed)); @@ -987,4 +1023,4 @@ pg_buffercache_lookup_table_entries(PG_FUNCTION_ARGS) BufTableGetContents(rsinfo->setResult, rsinfo->setDesc); return (Datum) 0; -} +} \ No newline at end of file diff --git a/contrib/pg_buffercache/t/001_basic.pl b/contrib/pg_buffercache/t/001_basic.pl new file mode 100644 index 0000000000000..d874b73d1268f --- /dev/null +++ b/contrib/pg_buffercache/t/001_basic.pl @@ -0,0 +1,244 @@ +# Copyright (c) 2025-2025, PostgreSQL Global Development Group +# +# Test shared_buffer resizing coordination with client connections joining using injection points + +use strict; +use warnings; +use IPC::Run; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use Time::HiRes qw(sleep); + +# Function to calculate expected buffer count from size string +sub calculate_buffer_count +{ + my ($size_string, $block_size) = @_; + + # Parse size and convert to bytes + my ($size_val, $unit) = ($size_string =~ /(\d+)(\w+)/); + my $size_bytes; + if (lc($unit) eq 'kb') { + $size_bytes = $size_val * 1024; + } elsif (lc($unit) eq 'mb') { + $size_bytes = $size_val * 1024 * 1024; + } elsif (lc($unit) eq 'gb') { + $size_bytes = $size_val * 1024 * 1024 * 1024; + } else { + # Default to kB if unit is not recognized + $size_bytes = $size_val * 1024; + } + + return int($size_bytes / $block_size); +} + +# Initialize cluster with very small buffer sizes for testing +my $node = PostgreSQL::Test::Cluster->new('main'); +my $shared_buffers_initial = '128MB'; +$node->init; + +# Configure for buffer resizing with very small buffer pool sizes for faster tests. +$node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points'); +$node->append_conf('postgresql.conf', qq{ +max_shared_buffers = '128MB' +shared_buffers = $shared_buffers_initial +max_parallel_workers_per_gather = 0 +restart_after_crash = off +log_min_messages = debug1 +}); + +$node->start; + +# Enable injection points +$node->safe_psql('postgres', "CREATE EXTENSION injection_points"); + +# Get the block size (this is fixed for the binary) +my $block_size = $node->safe_psql('postgres', "SHOW block_size"); + +# Try to create pg_buffercache extension for buffer analysis +eval { + $node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache"); +}; +if ($@) { + $node->stop; + plan skip_all => 'pg_buffercache extension not available - cannot verify buffer usage'; +} + +# Create dedicated sessions for injection point handling and test queries, +# so that we don't create new backends for test operations after starting +# resize operation. Only one backend, which tests new backend synchronization +# with resizing operation, should start after resizing has commenced. +my $injection_session = $node->background_psql('postgres'); +my $query_session = $node->background_psql('postgres'); +my $resize_session = $node->background_psql('postgres'); + +# Function to run a single injection point test +sub run_injection_point_test +{ + my ($test_name, $injection_point, $target_size, $operation_type) = @_; + + note("Test with $test_name ($operation_type)"); + + # Update buffer pool size and wait for it to reflect pending state + $resize_session->query_safe("ALTER SYSTEM SET shared_buffers = '$target_size'"); + $resize_session->query_safe("SELECT pg_reload_conf()"); + my $pending_size_str = "pending: $target_size"; + $resize_session->poll_query_until("SELECT substring(current_setting('shared_buffers'), '$pending_size_str')", $pending_size_str); + + # Set up injection point in injection session + $injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')"); + # Trigger resize + $resize_session->query_until( + qr/starting_resize/, + q( + \echo starting_resize + SELECT pg_resize_shared_buffers(); + ) + ); + + # Wait until resize actually reaches the injection point using the query session + $query_session->wait_for_event('client backend', $injection_point); + + # Start bufferscan while resize is paused + my $client = $node->background_psql('postgres'); + + # Wake up the injection point from injection session + $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')"); + + # Wait for the resize operation to complete + $resize_session->query(q(\echo 'done')); + + # Detach injection point from injection session + $injection_session->query_safe("SELECT injection_points_detach('$injection_point')"); + + # Verify resize completed successfully + is($query_session->query_safe("SELECT current_setting('shared_buffers')"), $target_size, + "resize completed successfully to $target_size"); + + # Check buffer pool size using pg_buffercache after resize completion + is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache"), calculate_buffer_count($target_size, $block_size), "pg_buffercache COUNT(*) correct after $test_name ($operation_type)"); + + # Wait for client to complete + ok($client->quit, "client succeeded during $test_name ($operation_type)"); +} + +# Test injection points during buffer resize with client connections +my @common_injection_tests = ( + { + name => 'flag setting phase', + injection_point => 'pg-resize-shared-buffers-flag-set', + }, + { + name => 'memory remap phase', + injection_point => 'pgrsb-after-shmem-resize', + }, + { + name => 'resize map barrier complete', + injection_point => 'pgrsb-resize-barrier-sent', + }, +); + +# Test common injection points for both shrinking and expanding +foreach my $test (@common_injection_tests) +{ + # Test shrinking scenario + run_injection_point_test($test->{name}, $test->{injection_point}, '272kB', 'shrinking'); + + # Test expanding scenario + run_injection_point_test($test->{name}, $test->{injection_point}, '400kB', 'expanding'); +} + +my @shrink_only_tests = ( + { + name => 'shrink barrier complete', + injection_point => 'pgrsb-shrink-barrier-sent', + size => '200kB', + } +); +foreach my $test (@shrink_only_tests) +{ + run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'shrinking only'); +} + +my @expand_only_tests = ( + { + name => 'expand barrier complete', + injection_point => 'pgrsb-expand-barrier-sent', + size => '416kB', + } +); +foreach my $test (@expand_only_tests) +{ + run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'expanding only'); +} + +# Function to test buffercache scan behavior during resize operations +# This tests that pg_buffercache correctly handles concurrent resize operations +# by pausing the buffercache scan at various points while a resize occurs. +# The expected behavior is that pg_buffercache detects the resize and raises +# an appropriate error "number of shared buffers changed during scan". +sub run_buffercache_injection_test +{ + my ($test_name, $buffercache_injection_point, $target_size, $operation_type) = @_; + + note("Test buffercache with $test_name ($operation_type)"); + + # Attach injection point at middle of buffercache scan + $node->safe_psql('postgres', "SELECT injection_points_attach('$buffercache_injection_point', 'wait')"); + + # Start buffercache query in background - it will pause at injection point + my $buffercache_session = $node->background_psql('postgres'); + $buffercache_session->query_until( + qr/starting_buffercache/, + q( + \echo starting_buffercache + SELECT COUNT(*) FROM pg_buffercache; + ) + ); + + # Wait for buffercache to reach injection point + $node->wait_for_event('client backend', $buffercache_injection_point); + + # Change shared_buffers to target size and resize + $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); + $node->safe_psql('postgres', "SELECT pg_reload_conf()"); + $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"); + + # Wake up buffercache scan + $node->safe_psql('postgres', "SELECT injection_points_wakeup('$buffercache_injection_point')"); + + $buffercache_session->query(q(\echo 'done')); + eval { $buffercache_session->quit; }; + eval { $node->safe_psql('postgres', "SELECT injection_points_detach('$buffercache_injection_point')"); }; + + # Verify server is still running + my $result; + eval { $result = $node->safe_psql('postgres', "SELECT COUNT(*) FROM pg_buffercache"); }; + is($result, calculate_buffer_count($target_size, $block_size), "Server still running after $test_name ($operation_type)"); +} + +# Test buffercache injection points - pausing buffercache while resize occurs +my @buffercache_injection_tests = ( + # { + # name => 'before the buffer pool scan starts', + # injection_point => 'pg-buffercache-scan-start', + # }, # Basic fail where after buffer change there are valid buffers (NOTE : Buffer fails after a little later then actual currentNBuffers Why?) + { + name => 'before getting buffer description - 2', + injection_point => 'pg-buffercache-after-getdesc', + }, # Failure where after buffer change there are no valid buffers; + +foreach my $test (@buffercache_injection_tests) +{ + # Test with shrinking + run_buffercache_injection_test($test->{name}, $test->{injection_point}, '256kB', 'shrinking'); + + # Test with expanding + run_buffercache_injection_test($test->{name}, $test->{injection_point}, '384kB', 'expanding'); +} + +$injection_session->quit; +$query_session->quit; +$resize_session->quit; + +done_testing(); \ No newline at end of file From ee59203ca8070be08ad09fe2e68d39c7cd929b06 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Thu, 26 Feb 2026 17:55:18 +0000 Subject: [PATCH 2/9] Refactor buffer cache tests to use smaller initial shared buffer size and improve session handling --- contrib/pg_buffercache/pg_buffercache_pages.c | 8 ++--- contrib/pg_buffercache/t/001_basic.pl | 34 ++++++++----------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index 745d07f836875..0ca7cef9ced0f 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -218,10 +218,10 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) * happen if only we setup the descriptor array large enough at * the server startup time. */ - // if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) - // ereport(ERROR, - // (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - // errmsg("number of shared buffers changed during scan of buffer cache"))); + if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("number of shared buffers changed during scan of buffer cache"))); elog(DEBUG1, "scanning buffer %d", i); diff --git a/contrib/pg_buffercache/t/001_basic.pl b/contrib/pg_buffercache/t/001_basic.pl index d874b73d1268f..fe8f5a5d02579 100644 --- a/contrib/pg_buffercache/t/001_basic.pl +++ b/contrib/pg_buffercache/t/001_basic.pl @@ -32,15 +32,15 @@ sub calculate_buffer_count return int($size_bytes / $block_size); } -# Initialize cluster with very small buffer sizes for testing +# Initialize cluster with #512 shared buffers so that buffer validity can be checked after half the buffers my $node = PostgreSQL::Test::Cluster->new('main'); -my $shared_buffers_initial = '128MB'; +my $shared_buffers_initial = '4MB'; $node->init; -# Configure for buffer resizing with very small buffer pool sizes for faster tests. +# Configure for buffer resizing with small buffer pool sizes for faster tests. $node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points'); $node->append_conf('postgresql.conf', qq{ -max_shared_buffers = '128MB' +max_shared_buffers = $shared_buffers_initial shared_buffers = $shared_buffers_initial max_parallel_workers_per_gather = 0 restart_after_crash = off @@ -59,18 +59,10 @@ sub calculate_buffer_count eval { $node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache"); }; -if ($@) { - $node->stop; - plan skip_all => 'pg_buffercache extension not available - cannot verify buffer usage'; -} -# Create dedicated sessions for injection point handling and test queries, -# so that we don't create new backends for test operations after starting -# resize operation. Only one backend, which tests new backend synchronization -# with resizing operation, should start after resizing has commenced. +# Create dedicated sessions for injection point handling and test queries. my $injection_session = $node->background_psql('postgres'); my $query_session = $node->background_psql('postgres'); -my $resize_session = $node->background_psql('postgres'); # Function to run a single injection point test sub run_injection_point_test @@ -79,14 +71,15 @@ sub run_injection_point_test note("Test with $test_name ($operation_type)"); - # Update buffer pool size and wait for it to reflect pending state - $resize_session->query_safe("ALTER SYSTEM SET shared_buffers = '$target_size'"); - $resize_session->query_safe("SELECT pg_reload_conf()"); - my $pending_size_str = "pending: $target_size"; - $resize_session->poll_query_until("SELECT substring(current_setting('shared_buffers'), '$pending_size_str')", $pending_size_str); + # Update buffer pool size + $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); + $node->safe_psql('postgres', "SELECT pg_reload_conf()"); # Set up injection point in injection session $injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')"); + + # Create a new session for resize - it picks up new config automatically + my $resize_session = $node->background_psql('postgres'); # Trigger resize $resize_session->query_until( qr/starting_resize/, @@ -101,12 +94,16 @@ sub run_injection_point_test # Start bufferscan while resize is paused my $client = $node->background_psql('postgres'); + note("Background client backend PID: " . $client->query_safe("SELECT pg_backend_pid()")); # Wake up the injection point from injection session $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')"); + $client->query_safe("SELECT count(*) FROM pg_buffercache"); + # Wait for the resize operation to complete $resize_session->query(q(\echo 'done')); + $resize_session->quit; # Detach injection point from injection session $injection_session->query_safe("SELECT injection_points_detach('$injection_point')"); @@ -239,6 +236,5 @@ sub run_buffercache_injection_test $injection_session->quit; $query_session->quit; -$resize_session->quit; done_testing(); \ No newline at end of file From ff378cbc6c562534405c25f5d8d8b9bc9d565752 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Thu, 5 Mar 2026 17:56:50 +0000 Subject: [PATCH 3/9] Fix comments --- contrib/pg_buffercache/pg_buffercache_pages.c | 3 - contrib/pg_buffercache/t/001_basic.pl | 257 ++++++++++-------- 2 files changed, 144 insertions(+), 116 deletions(-) diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index 0ca7cef9ced0f..d0246a080d469 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -222,8 +222,6 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("number of shared buffers changed during scan of buffer cache"))); - - elog(DEBUG1, "scanning buffer %d", i); bufHdr = GetBufferDescriptor(i); @@ -236,7 +234,6 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) * One injection point before locking buffer descriptor helps covers all the later cases. */ buf_state = LockBufHdr(bufHdr); - elog(DEBUG1, "got buffer descriptor for buffer %d", i); fctx->record[i].bufferid = BufferDescriptorGetBuffer(bufHdr); fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); fctx->record[i].reltablespace = bufHdr->tag.spcOid; diff --git a/contrib/pg_buffercache/t/001_basic.pl b/contrib/pg_buffercache/t/001_basic.pl index fe8f5a5d02579..1f3986d860738 100644 --- a/contrib/pg_buffercache/t/001_basic.pl +++ b/contrib/pg_buffercache/t/001_basic.pl @@ -1,6 +1,13 @@ # Copyright (c) 2025-2025, PostgreSQL Global Development Group # -# Test shared_buffer resizing coordination with client connections joining using injection points +# Test pg_buffercache scan behavior during shared buffer pool resizing. +# +# Two categories of tests: +# 1. pg_buffercache scan while resize is paused at various injection points +# -- verifies that a concurrent scan succeeds and reports the correct +# buffer count after resize completes. +# 2. pg_buffercache scan paused mid-scan while a resize occurs underneath +# -- verifies that the scan detects the change and raises an error. use strict; use warnings; @@ -10,77 +17,73 @@ use Test::More; use Time::HiRes qw(sleep); -# Function to calculate expected buffer count from size string +# Skip this test if injection points are not supported +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# Convert a human-readable size string (e.g. "256kB", "8MB") to the expected +# number of buffers for the given block_size. sub calculate_buffer_count { my ($size_string, $block_size) = @_; - - # Parse size and convert to bytes + my ($size_val, $unit) = ($size_string =~ /(\d+)(\w+)/); my $size_bytes; - if (lc($unit) eq 'kb') { - $size_bytes = $size_val * 1024; - } elsif (lc($unit) eq 'mb') { - $size_bytes = $size_val * 1024 * 1024; - } elsif (lc($unit) eq 'gb') { - $size_bytes = $size_val * 1024 * 1024 * 1024; - } else { - # Default to kB if unit is not recognized - $size_bytes = $size_val * 1024; - } - + if (lc($unit) eq 'kb') { $size_bytes = $size_val * 1024; } + elsif (lc($unit) eq 'mb') { $size_bytes = $size_val * 1024 * 1024; } + elsif (lc($unit) eq 'gb') { $size_bytes = $size_val * 1024 * 1024 * 1024; } + else { $size_bytes = $size_val * 1024; } + return int($size_bytes / $block_size); } -# Initialize cluster with #512 shared buffers so that buffer validity can be checked after half the buffers +# Set up the test cluster with a small buffer pool. my $node = PostgreSQL::Test::Cluster->new('main'); -my $shared_buffers_initial = '4MB'; +my $shared_buffers_initial = '8MB'; $node->init; - -# Configure for buffer resizing with small buffer pool sizes for faster tests. -$node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points'); $node->append_conf('postgresql.conf', qq{ -max_shared_buffers = $shared_buffers_initial -shared_buffers = $shared_buffers_initial -max_parallel_workers_per_gather = 0 -restart_after_crash = off -log_min_messages = debug1 + shared_preload_libraries = 'injection_points' + max_shared_buffers = $shared_buffers_initial + shared_buffers = $shared_buffers_initial + max_parallel_workers_per_gather = 0 + restart_after_crash = off }); - $node->start; -# Enable injection points $node->safe_psql('postgres', "CREATE EXTENSION injection_points"); +$node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache"); -# Get the block size (this is fixed for the binary) my $block_size = $node->safe_psql('postgres', "SHOW block_size"); -# Try to create pg_buffercache extension for buffer analysis -eval { - $node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache"); -}; - -# Create dedicated sessions for injection point handling and test queries. +# Long-lived sessions reused across tests to avoid creating new backends +# during resize operations. my $injection_session = $node->background_psql('postgres'); -my $query_session = $node->background_psql('postgres'); - -# Function to run a single injection point test -sub run_injection_point_test +my $query_session = $node->background_psql('postgres'); + +############################################################################## +# Test 1: pg_buffercache scan while resize is paused +# +# Pause the resize operation at an injection point, run a full pg_buffercache +# scan from a separate client, then let the resize finish. Verify the scan +# succeeds and the final buffer count matches the target size. +############################################################################## +sub run_resize_injection_test { my ($test_name, $injection_point, $target_size, $operation_type) = @_; - - note("Test with $test_name ($operation_type)"); - - # Update buffer pool size - $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); + + note("Test: $test_name ($operation_type) -> $target_size"); + + $node->safe_psql('postgres', + "ALTER SYSTEM SET shared_buffers = '$target_size'"); $node->safe_psql('postgres', "SELECT pg_reload_conf()"); - # Set up injection point in injection session - $injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')"); + $injection_session->query_safe( + "SELECT injection_points_attach('$injection_point', 'wait')"); - # Create a new session for resize - it picks up new config automatically + # Trigger resize in a dedicated session. my $resize_session = $node->background_psql('postgres'); - # Trigger resize $resize_session->query_until( qr/starting_resize/, q( @@ -88,38 +91,39 @@ sub run_injection_point_test SELECT pg_resize_shared_buffers(); ) ); - - # Wait until resize actually reaches the injection point using the query session + + # Wait for the resize backend to hit the injection point. $query_session->wait_for_event('client backend', $injection_point); - - # Start bufferscan while resize is paused + + # Run a full pg_buffercache scan while resize is paused. my $client = $node->background_psql('postgres'); - note("Background client backend PID: " . $client->query_safe("SELECT pg_backend_pid()")); - - # Wake up the injection point from injection session - $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')"); - $client->query_safe("SELECT count(*) FROM pg_buffercache"); - - # Wait for the resize operation to complete + + # Resume resize. + $injection_session->query_safe( + "SELECT injection_points_wakeup('$injection_point')"); + $resize_session->query(q(\echo 'done')); $resize_session->quit; - - # Detach injection point from injection session - $injection_session->query_safe("SELECT injection_points_detach('$injection_point')"); - - # Verify resize completed successfully - is($query_session->query_safe("SELECT current_setting('shared_buffers')"), $target_size, - "resize completed successfully to $target_size"); - - # Check buffer pool size using pg_buffercache after resize completion - is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache"), calculate_buffer_count($target_size, $block_size), "pg_buffercache COUNT(*) correct after $test_name ($operation_type)"); - - # Wait for client to complete - ok($client->quit, "client succeeded during $test_name ($operation_type)"); + + $injection_session->query_safe( + "SELECT injection_points_detach('$injection_point')"); + + # Verify the resize landed and the buffer count is correct. + is($query_session->query_safe( + "SELECT current_setting('shared_buffers')"), + $target_size, + "shared_buffers is $target_size after $test_name ($operation_type)"); + + is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache"), + calculate_buffer_count($target_size, $block_size), + "pg_buffercache count matches after $test_name ($operation_type)"); + + ok($client->quit, + "client scan succeeded during $test_name ($operation_type)"); } -# Test injection points during buffer resize with client connections +# Injection points common to both shrink and expand paths. my @common_injection_tests = ( { name => 'flag setting phase', @@ -139,10 +143,12 @@ sub run_injection_point_test foreach my $test (@common_injection_tests) { # Test shrinking scenario - run_injection_point_test($test->{name}, $test->{injection_point}, '272kB', 'shrinking'); + run_resize_injection_test( + $test->{name}, $test->{injection_point}, '272kB', 'shrinking'); # Test expanding scenario - run_injection_point_test($test->{name}, $test->{injection_point}, '400kB', 'expanding'); + run_resize_injection_test( + $test->{name}, $test->{injection_point}, '400kB', 'expanding'); } my @shrink_only_tests = ( @@ -150,88 +156,113 @@ sub run_injection_point_test name => 'shrink barrier complete', injection_point => 'pgrsb-shrink-barrier-sent', size => '200kB', - } + }, ); foreach my $test (@shrink_only_tests) { - run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'shrinking only'); + run_resize_injection_test( + $test->{name}, $test->{injection_point}, $test->{size}, 'shrinking'); } my @expand_only_tests = ( { name => 'expand barrier complete', injection_point => 'pgrsb-expand-barrier-sent', - size => '416kB', - } + size => '8MB', + }, ); foreach my $test (@expand_only_tests) { - run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'expanding only'); + run_resize_injection_test( + $test->{name}, $test->{injection_point}, $test->{size}, 'expanding'); } -# Function to test buffercache scan behavior during resize operations -# This tests that pg_buffercache correctly handles concurrent resize operations -# by pausing the buffercache scan at various points while a resize occurs. -# The expected behavior is that pg_buffercache detects the resize and raises -# an appropriate error "number of shared buffers changed during scan". -sub run_buffercache_injection_test +############################################################################## +# Test 2: resize while pg_buffercache scan is in progress +# +# Pause a pg_buffercache scan mid-flight using an injection point inside the +# scan itself, then resize the buffer pool underneath. The scan should detect +# the NBuffers change and raise: +# ERROR: number of shared buffers changed during scan of buffer cache +############################################################################## +sub run_buffercache_scan_error_test { - my ($test_name, $buffercache_injection_point, $target_size, $operation_type) = @_; - - note("Test buffercache with $test_name ($operation_type)"); + my ($test_name, $scan_injection_point, $target_size, $operation_type) = @_; - # Attach injection point at middle of buffercache scan - $node->safe_psql('postgres', "SELECT injection_points_attach('$buffercache_injection_point', 'wait')"); + note("Test: $test_name ($operation_type) -> $target_size"); + + # Pause the pg_buffercache scan at the given injection point. + $node->safe_psql('postgres', + "SELECT injection_points_attach('$scan_injection_point', 'wait')"); - # Start buffercache query in background - it will pause at injection point my $buffercache_session = $node->background_psql('postgres'); $buffercache_session->query_until( qr/starting_buffercache/, - q( - \echo starting_buffercache - SELECT COUNT(*) FROM pg_buffercache; - ) + "\\echo starting_buffercache\nSELECT COUNT(*) FROM pg_buffercache;\n" ); - # Wait for buffercache to reach injection point - $node->wait_for_event('client backend', $buffercache_injection_point); + $node->wait_for_event('client backend', $scan_injection_point); - # Change shared_buffers to target size and resize - $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); + # Resize the buffer pool while the scan is paused. + $node->safe_psql('postgres', + "ALTER SYSTEM SET shared_buffers = '$target_size'"); $node->safe_psql('postgres', "SELECT pg_reload_conf()"); $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"); - # Wake up buffercache scan - $node->safe_psql('postgres', "SELECT injection_points_wakeup('$buffercache_injection_point')"); + # Resume the scan -- it should now see the mismatch and error out. + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('$scan_injection_point')"); + + # Drain output from the background session. The psql process will exit + # after the ERROR, so pump until it is no longer pumpable. + eval { + while ($buffercache_session->{run}->pumpable()) + { + $buffercache_session->{run}->pump_nb(); + last if $buffercache_session->{stderr} =~ /ERROR/; + Time::HiRes::sleep(0.01); + } + $buffercache_session->{run}->pump_nb() + if $buffercache_session->{run}->pumpable(); + }; + + note("pg_buffercache scan output:\n" . $buffercache_session->{stdout}); + note("pg_buffercache scan error output:\n" . $buffercache_session->{stderr}); - $buffercache_session->query(q(\echo 'done')); eval { $buffercache_session->quit; }; - eval { $node->safe_psql('postgres', "SELECT injection_points_detach('$buffercache_injection_point')"); }; + eval { + $node->safe_psql('postgres', + "SELECT injection_points_detach('$scan_injection_point')"); + }; - # Verify server is still running - my $result; - eval { $result = $node->safe_psql('postgres', "SELECT COUNT(*) FROM pg_buffercache"); }; - is($result, calculate_buffer_count($target_size, $block_size), "Server still running after $test_name ($operation_type)"); + # Confirm the server is functional and the resize took effect. + my $result = $node->safe_psql('postgres', + "SELECT COUNT(*) FROM pg_buffercache"); + is($result, calculate_buffer_count($target_size, $block_size), + "buffer count correct after $test_name ($operation_type)"); } # Test buffercache injection points - pausing buffercache while resize occurs -my @buffercache_injection_tests = ( +my @buffercache_scan_tests = ( # { # name => 'before the buffer pool scan starts', # injection_point => 'pg-buffercache-scan-start', # }, # Basic fail where after buffer change there are valid buffers (NOTE : Buffer fails after a little later then actual currentNBuffers Why?) { - name => 'before getting buffer description - 2', + name => 'before getting buffer description', injection_point => 'pg-buffercache-after-getdesc', }, # Failure where after buffer change there are no valid buffers; +); -foreach my $test (@buffercache_injection_tests) +foreach my $test (@buffercache_scan_tests) { # Test with shrinking - run_buffercache_injection_test($test->{name}, $test->{injection_point}, '256kB', 'shrinking'); - + run_buffercache_scan_error_test( + $test->{name}, $test->{injection_point}, '256kB', 'shrinking'); + # Test with expanding - run_buffercache_injection_test($test->{name}, $test->{injection_point}, '384kB', 'expanding'); + run_buffercache_scan_error_test( + $test->{name}, $test->{injection_point}, '384kB', 'expanding'); } $injection_session->quit; From 954777010a35998a0ec9168f6054048a94e16c64 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Wed, 18 Mar 2026 19:14:57 +0000 Subject: [PATCH 4/9] Addressing comments --- contrib/pg_buffercache/t/001_basic.pl | 251 ++++++++++++-------------- 1 file changed, 115 insertions(+), 136 deletions(-) diff --git a/contrib/pg_buffercache/t/001_basic.pl b/contrib/pg_buffercache/t/001_basic.pl index 1f3986d860738..ecbefbbed6226 100644 --- a/contrib/pg_buffercache/t/001_basic.pl +++ b/contrib/pg_buffercache/t/001_basic.pl @@ -1,21 +1,13 @@ # Copyright (c) 2025-2025, PostgreSQL Global Development Group # -# Test pg_buffercache scan behavior during shared buffer pool resizing. -# -# Two categories of tests: -# 1. pg_buffercache scan while resize is paused at various injection points -# -- verifies that a concurrent scan succeeds and reports the correct -# buffer count after resize completes. -# 2. pg_buffercache scan paused mid-scan while a resize occurs underneath -# -- verifies that the scan detects the change and raises an error. +# Test pg_buffercache scan behavior during shared_buffer resizing using +# injection points. use strict; use warnings; -use IPC::Run; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; -use Time::HiRes qw(sleep); # Skip this test if injection points are not supported if ($ENV{enable_injection_points} ne 'yes') @@ -23,23 +15,6 @@ plan skip_all => 'Injection points not supported by this build'; } -# Convert a human-readable size string (e.g. "256kB", "8MB") to the expected -# number of buffers for the given block_size. -sub calculate_buffer_count -{ - my ($size_string, $block_size) = @_; - - my ($size_val, $unit) = ($size_string =~ /(\d+)(\w+)/); - my $size_bytes; - if (lc($unit) eq 'kb') { $size_bytes = $size_val * 1024; } - elsif (lc($unit) eq 'mb') { $size_bytes = $size_val * 1024 * 1024; } - elsif (lc($unit) eq 'gb') { $size_bytes = $size_val * 1024 * 1024 * 1024; } - else { $size_bytes = $size_val * 1024; } - - return int($size_bytes / $block_size); -} - -# Set up the test cluster with a small buffer pool. my $node = PostgreSQL::Test::Cluster->new('main'); my $shared_buffers_initial = '8MB'; $node->init; @@ -52,37 +27,39 @@ sub calculate_buffer_count }); $node->start; +# Load injection_points and pg_buffercache extensions $node->safe_psql('postgres', "CREATE EXTENSION injection_points"); $node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache"); -my $block_size = $node->safe_psql('postgres', "SHOW block_size"); - -# Long-lived sessions reused across tests to avoid creating new backends -# during resize operations. +# Create dedicated sessions for injection point handling and test queries, +# so that we don't create new backends for test operations after starting +# resize operation. my $injection_session = $node->background_psql('postgres'); -my $query_session = $node->background_psql('postgres'); +my $query_session = $node->background_psql('postgres'); +my $resize_session = $node->background_psql('postgres'); -############################################################################## -# Test 1: pg_buffercache scan while resize is paused -# -# Pause the resize operation at an injection point, run a full pg_buffercache -# scan from a separate client, then let the resize finish. Verify the scan -# succeeds and the final buffer count matches the target size. -############################################################################## -sub run_resize_injection_test +# Function to run a single injection point test. +sub run_injection_point_test { my ($test_name, $injection_point, $target_size, $operation_type) = @_; - note("Test: $test_name ($operation_type) -> $target_size"); + # Silence the logging of the statements we run to avoid + # unnecessarily bloating the test logs. This runs before the + # upgrade we're testing, so the details should not be very + # interesting for debugging. But if needed, you can make it more + # verbose by setting this. + my $verbose = 0; - $node->safe_psql('postgres', - "ALTER SYSTEM SET shared_buffers = '$target_size'"); - $node->safe_psql('postgres', "SELECT pg_reload_conf()"); + note("Test with $test_name ($operation_type)"); - $injection_session->query_safe( - "SELECT injection_points_attach('$injection_point', 'wait')"); + # Update buffer pool size + $resize_session->query_safe("ALTER SYSTEM SET shared_buffers = '$target_size'", verbose => $verbose); + $resize_session->query_safe("SELECT pg_reload_conf()", verbose => $verbose); + + # Set up injection point in injection session + $injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')", verbose => $verbose); - # Trigger resize in a dedicated session. + # Create a new session for resize my $resize_session = $node->background_psql('postgres'); $resize_session->query_until( qr/starting_resize/, @@ -92,38 +69,40 @@ sub run_resize_injection_test ) ); - # Wait for the resize backend to hit the injection point. - $query_session->wait_for_event('client backend', $injection_point); + # Wait until resize actually reaches the injection point + $query_session->wait_for_event('client backend', $injection_point, verbose => $verbose); - # Run a full pg_buffercache scan while resize is paused. + # Start bufferscan while resize is paused my $client = $node->background_psql('postgres'); - $client->query_safe("SELECT count(*) FROM pg_buffercache"); + $client->query_safe("SELECT count(*) FROM pg_buffercache", verbose => $verbose); - # Resume resize. - $injection_session->query_safe( - "SELECT injection_points_wakeup('$injection_point')"); + # Wake up the injection point from injection session + $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')", verbose => $verbose); - $resize_session->query(q(\echo 'done')); + # Wait for the resize operation to complete + $resize_session->query(q(\echo 'done'), verbose => $verbose); $resize_session->quit; - $injection_session->query_safe( - "SELECT injection_points_detach('$injection_point')"); + # Detach injection point from injection session + $injection_session->query_safe("SELECT injection_points_detach('$injection_point')",verbose => $verbose); - # Verify the resize landed and the buffer count is correct. - is($query_session->query_safe( - "SELECT current_setting('shared_buffers')"), - $target_size, - "shared_buffers is $target_size after $test_name ($operation_type)"); + # Verify resize completed successfully + is($query_session->query_safe("SELECT current_setting('shared_buffers')", verbose => $verbose), $target_size, + "resize completed successfully to $target_size"); - is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache"), - calculate_buffer_count($target_size, $block_size), - "pg_buffercache count matches after $test_name ($operation_type)"); + # Confirm the server is functional and the resize took effect. + my $result = $node->safe_psql('postgres', + "SELECT setting from pg_settings where name = 'shared_buffers'"); - ok($client->quit, - "client scan succeeded during $test_name ($operation_type)"); + # Check buffer pool size using pg_buffercache after resize completion + is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", verbose => $verbose), + $result, "pg_buffercache COUNT(*) correct after $test_name ($operation_type)"); + + # Wait for client to complete + ok($client->quit, "client succeeded during $test_name ($operation_type)"); } -# Injection points common to both shrink and expand paths. +# Test injection points during buffer resize with client connections my @common_injection_tests = ( { name => 'flag setting phase', @@ -143,12 +122,10 @@ sub run_resize_injection_test foreach my $test (@common_injection_tests) { # Test shrinking scenario - run_resize_injection_test( - $test->{name}, $test->{injection_point}, '272kB', 'shrinking'); + run_injection_point_test($test->{name}, $test->{injection_point}, '272kB', 'shrinking'); # Test expanding scenario - run_resize_injection_test( - $test->{name}, $test->{injection_point}, '400kB', 'expanding'); + run_injection_point_test($test->{name}, $test->{injection_point}, '400kB', 'expanding'); } my @shrink_only_tests = ( @@ -156,113 +133,115 @@ sub run_resize_injection_test name => 'shrink barrier complete', injection_point => 'pgrsb-shrink-barrier-sent', size => '200kB', - }, + } ); foreach my $test (@shrink_only_tests) { - run_resize_injection_test( - $test->{name}, $test->{injection_point}, $test->{size}, 'shrinking'); + run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'shrinking only'); } my @expand_only_tests = ( { name => 'expand barrier complete', injection_point => 'pgrsb-expand-barrier-sent', - size => '8MB', - }, + size => '416kB', + } ); foreach my $test (@expand_only_tests) { - run_resize_injection_test( - $test->{name}, $test->{injection_point}, $test->{size}, 'expanding'); + run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'expanding only'); } -############################################################################## -# Test 2: resize while pg_buffercache scan is in progress -# -# Pause a pg_buffercache scan mid-flight using an injection point inside the -# scan itself, then resize the buffer pool underneath. The scan should detect -# the NBuffers change and raise: -# ERROR: number of shared buffers changed during scan of buffer cache -############################################################################## -sub run_buffercache_scan_error_test +# Function to test buffercache scan behavior during resize operations. +sub run_buffercache_injection_test { - my ($test_name, $scan_injection_point, $target_size, $operation_type) = @_; + my ($test_name, $buffercache_injection_point, $target_size, $operation_type) = @_; - note("Test: $test_name ($operation_type) -> $target_size"); + my $verbose = 0; - # Pause the pg_buffercache scan at the given injection point. - $node->safe_psql('postgres', - "SELECT injection_points_attach('$scan_injection_point', 'wait')"); + note("Test buffercache with $test_name ($operation_type)"); - my $buffercache_session = $node->background_psql('postgres'); + # Attach injection point at the buffercache scan + $injection_session->query_safe( + "SELECT injection_points_attach('$buffercache_injection_point', 'wait')", + verbose => $verbose); + + # Start buffercache query in background - it will pause at injection point. + # Use on_error_stop => 0 so psql stays alive if the query errors out. + my $buffercache_session = $node->background_psql('postgres', on_error_stop => 0); $buffercache_session->query_until( qr/starting_buffercache/, - "\\echo starting_buffercache\nSELECT COUNT(*) FROM pg_buffercache;\n" + q( + \echo starting_buffercache + SELECT COUNT(*) FROM pg_buffercache; + ) ); - $node->wait_for_event('client backend', $scan_injection_point); + # Wait for buffercache to reach injection point + $query_session->wait_for_event('client backend', $buffercache_injection_point, + verbose => $verbose); - # Resize the buffer pool while the scan is paused. - $node->safe_psql('postgres', - "ALTER SYSTEM SET shared_buffers = '$target_size'"); + # Change shared_buffers to target size and reload config + $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); $node->safe_psql('postgres', "SELECT pg_reload_conf()"); - $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"); - # Resume the scan -- it should now see the mismatch and error out. - $node->safe_psql('postgres', - "SELECT injection_points_wakeup('$scan_injection_point')"); + # Start a new background session for resize + $resize_session->query_safe("SELECT pg_resize_shared_buffers()", + verbose => $verbose); + $resize_session->quit; - # Drain output from the background session. The psql process will exit - # after the ERROR, so pump until it is no longer pumpable. - eval { - while ($buffercache_session->{run}->pumpable()) - { - $buffercache_session->{run}->pump_nb(); - last if $buffercache_session->{stderr} =~ /ERROR/; - Time::HiRes::sleep(0.01); - } - $buffercache_session->{run}->pump_nb() - if $buffercache_session->{run}->pumpable(); - }; + # Wake up buffercache scan and collect its results + $injection_session->query_safe( + "SELECT injection_points_wakeup('$buffercache_injection_point')", + verbose => $verbose); + + my ($buffercache_output, $buffercache_err); + eval { ($buffercache_output, $buffercache_err) = $buffercache_session->query(q(\echo 'done')); }; + my $query_err = $@; - note("pg_buffercache scan output:\n" . $buffercache_session->{stdout}); - note("pg_buffercache scan error output:\n" . $buffercache_session->{stderr}); + # Print the stderr/stdout output (even if query() died due to crash) + note("buffercache stderr during $test_name ($operation_type): \n" . ($buffercache_session->{stderr} // '')); + note("buffercache stdout during $test_name ($operation_type): \n" . ($buffercache_output // '')); + note("buffercache query error during $test_name ($operation_type): $query_err") if $query_err; + $buffercache_session->{stderr} = ''; eval { $buffercache_session->quit; }; + + # Detach injection point eval { - $node->safe_psql('postgres', - "SELECT injection_points_detach('$scan_injection_point')"); + $injection_session->query_safe( + "SELECT injection_points_detach('$buffercache_injection_point')", + verbose => $verbose); }; # Confirm the server is functional and the resize took effect. - my $result = $node->safe_psql('postgres', - "SELECT COUNT(*) FROM pg_buffercache"); - is($result, calculate_buffer_count($target_size, $block_size), - "buffer count correct after $test_name ($operation_type)"); + my $result = $node->safe_psql('postgres', "SELECT setting from pg_settings where name = 'shared_buffers'"); + + is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", + verbose => $verbose), + $result, + "pg_buffercache count matches after $test_name ($operation_type)"); } # Test buffercache injection points - pausing buffercache while resize occurs -my @buffercache_scan_tests = ( - # { - # name => 'before the buffer pool scan starts', - # injection_point => 'pg-buffercache-scan-start', - # }, # Basic fail where after buffer change there are valid buffers (NOTE : Buffer fails after a little later then actual currentNBuffers Why?) +my @buffercache_injection_tests = ( { - name => 'before getting buffer description', - injection_point => 'pg-buffercache-after-getdesc', - }, # Failure where after buffer change there are no valid buffers; + name => 'before the buffer pool scan starts', + injection_point => 'pg-buffercache-scan-start', + }, # Basic fail where after buffer change there are valid buffers + # { + # name => 'before getting buffer description - 2', + # injection_point => 'pg-buffercache-after-getdesc', + # }, # Failure where after buffer change there are no valid buffers; ); -foreach my $test (@buffercache_scan_tests) +foreach my $test (@buffercache_injection_tests) { # Test with shrinking - run_buffercache_scan_error_test( - $test->{name}, $test->{injection_point}, '256kB', 'shrinking'); + run_buffercache_injection_test($test->{name}, $test->{injection_point}, '256kB', 'shrinking'); # Test with expanding - run_buffercache_scan_error_test( - $test->{name}, $test->{injection_point}, '384kB', 'expanding'); + run_buffercache_injection_test($test->{name}, $test->{injection_point}, '384kB', 'expanding'); } $injection_session->quit; From 4d0748fc95bae8b83857677eda27e691878613f6 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Thu, 19 Mar 2026 08:47:20 +0000 Subject: [PATCH 5/9] removing extra changes --- contrib/pg_buffercache/pg_buffercache_pages.c | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index d0246a080d469..36c37fe0be1db 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -222,7 +222,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("number of shared buffers changed during scan of buffer cache"))); - + bufHdr = GetBufferDescriptor(i); /* Injection point during scan to test resize interaction during buffer resize and accessing invalid buffers after resize in case of shrinking */ @@ -235,7 +235,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) */ buf_state = LockBufHdr(bufHdr); fctx->record[i].bufferid = BufferDescriptorGetBuffer(bufHdr); - fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); + fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); fctx->record[i].reltablespace = bufHdr->tag.spcOid; fctx->record[i].reldatabase = bufHdr->tag.dbOid; fctx->record[i].forknum = BufTagGetForkNum(&bufHdr->tag); @@ -515,11 +515,6 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) CHECK_FOR_INTERRUPTS(); - if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("number of shared buffers changed during scan of buffer cache"))); - bufHdr = GetBufferDescriptor(i); /* Lock each buffer header before inspecting. */ @@ -731,11 +726,6 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS) CHECK_FOR_INTERRUPTS(); - if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("number of shared buffers changed during scan of buffer cache"))); - usage_count = BUF_STATE_GET_USAGECOUNT(buf_state); usage_counts[usage_count]++; @@ -1020,4 +1010,4 @@ pg_buffercache_lookup_table_entries(PG_FUNCTION_ARGS) BufTableGetContents(rsinfo->setResult, rsinfo->setDesc); return (Datum) 0; -} \ No newline at end of file +} \ No newline at end of file From 477d6061c8d4cd3c0fb35cd29257feae9882e621 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Thu, 19 Mar 2026 08:55:18 +0000 Subject: [PATCH 6/9] Refactor pg_buffercache_pages.c to clean up includes --- contrib/pg_buffercache/pg_buffercache_pages.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index 36c37fe0be1db..e08a8e24dea22 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -15,10 +15,9 @@ #include "port/pg_numa.h" #include "storage/buf_internals.h" #include "storage/bufmgr.h" +#include "utils/injection_point.h" #include "utils/rel.h" #include "utils/tuplestore.h" -#include "utils/injection_point.h" - #define NUM_BUFFERCACHE_PAGES_MIN_ELEM 8 #define NUM_BUFFERCACHE_PAGES_ELEM 9 @@ -233,9 +232,11 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) * All the places where bufHdr is being called. * One injection point before locking buffer descriptor helps covers all the later cases. */ + + /* Lock each buffer header before inspecting. */ buf_state = LockBufHdr(bufHdr); fctx->record[i].bufferid = BufferDescriptorGetBuffer(bufHdr); - fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); + fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); fctx->record[i].reltablespace = bufHdr->tag.spcOid; fctx->record[i].reldatabase = bufHdr->tag.dbOid; fctx->record[i].forknum = BufTagGetForkNum(&bufHdr->tag); @@ -658,11 +659,6 @@ pg_buffercache_summary(PG_FUNCTION_ARGS) CHECK_FOR_INTERRUPTS(); - if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("number of shared buffers changed during scan of buffer cache"))); - /* * This function summarizes the state of all headers. Locking the * buffer headers wouldn't provide an improved result as the state of @@ -1010,4 +1006,4 @@ pg_buffercache_lookup_table_entries(PG_FUNCTION_ARGS) BufTableGetContents(rsinfo->setResult, rsinfo->setDesc); return (Datum) 0; -} \ No newline at end of file +} \ No newline at end of file From 2983975603f4705957789d2c5e2cc36682c334f3 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Fri, 27 Mar 2026 03:58:32 +0000 Subject: [PATCH 7/9] Refactor pg_buffercache Makefile and enhance injection point tests for buffer resizing scenarios --- contrib/pg_buffercache/Makefile | 3 - contrib/pg_buffercache/pg_buffercache_pages.c | 27 ++- contrib/pg_buffercache/t/001_basic.pl | 218 ++++++++++-------- 3 files changed, 135 insertions(+), 113 deletions(-) diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile index 566cc91118ba4..c632d8042959d 100644 --- a/contrib/pg_buffercache/Makefile +++ b/contrib/pg_buffercache/Makefile @@ -5,9 +5,6 @@ OBJS = \ $(WIN32RES) \ pg_buffercache_pages.o -EXTRA_INSTALL=src/test/modules/injection_points \ - contrib/test_decoding - EXTENSION = pg_buffercache DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \ pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \ diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index e08a8e24dea22..7197ef50eb355 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -201,7 +201,9 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) */ /* - * This point fails when lock bufHdr fails later because of invalid buffer after resize. + * Injection point before the scan loop. If the buffer pool is + * resized while we are paused here, the later LockBufHdr() call + * may access an invalid buffer descriptor. */ INJECTION_POINT("pg-buffercache-scan-start", NULL); for (i = 0; i < currentNBuffers; i++) @@ -223,18 +225,17 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) errmsg("number of shared buffers changed during scan of buffer cache"))); bufHdr = GetBufferDescriptor(i); - - /* Injection point during scan to test resize interaction during buffer resize and accessing invalid buffers after resize in case of shrinking */ - if(i==currentNBuffers/2) - INJECTION_POINT("pg-buffercache-after-getdesc", NULL); + /* - * Point of failure is when invalid buffer is accessed after resize. - * All the places where bufHdr is being called. - * One injection point before locking buffer descriptor helps covers all the later cases. - */ + * Injection point halfway through the scan, to test + * resize interaction while accessing buffer descriptors + * that may become invalid after a shrink. + */ + if (i == currentNBuffers / 2) + INJECTION_POINT("pg-buffercache-after-getdesc", NULL); /* Lock each buffer header before inspecting. */ - buf_state = LockBufHdr(bufHdr); + buf_state = LockBufHdr(bufHdr); fctx->record[i].bufferid = BufferDescriptorGetBuffer(bufHdr); fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); fctx->record[i].reltablespace = bufHdr->tag.spcOid; @@ -774,6 +775,12 @@ pg_buffercache_evict(PG_FUNCTION_ARGS) bool buffer_flushed; int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); + /* + * Injection point after reading currentNBuffers but before the + * bounds check. Allows testing the behavior when a resize occurs + * between reading the pool size and validating the buffer ID. + */ + INJECTION_POINT("pg-buffercache-evict-before-check", NULL); if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); diff --git a/contrib/pg_buffercache/t/001_basic.pl b/contrib/pg_buffercache/t/001_basic.pl index ecbefbbed6226..8b00c5569982f 100644 --- a/contrib/pg_buffercache/t/001_basic.pl +++ b/contrib/pg_buffercache/t/001_basic.pl @@ -22,7 +22,6 @@ shared_preload_libraries = 'injection_points' max_shared_buffers = $shared_buffers_initial shared_buffers = $shared_buffers_initial - max_parallel_workers_per_gather = 0 restart_after_crash = off }); $node->start; @@ -38,10 +37,13 @@ my $query_session = $node->background_psql('postgres'); my $resize_session = $node->background_psql('postgres'); -# Function to run a single injection point test. -sub run_injection_point_test +# Pause the buffer pool resize at the given injection point and run a +# pg_buffercache scan while the resize is paused. After the scan completes, +# wake up the resize operation and verify that both the resize and the scan +# produce correct results. +sub run_scan_during_paused_resize { - my ($test_name, $injection_point, $target_size, $operation_type) = @_; + my ($test_name, $injection_point, $target_size, $target_buffers, $operation_type) = @_; # Silence the logging of the statements we run to avoid # unnecessarily bloating the test logs. This runs before the @@ -59,8 +61,6 @@ sub run_injection_point_test # Set up injection point in injection session $injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')", verbose => $verbose); - # Create a new session for resize - my $resize_session = $node->background_psql('postgres'); $resize_session->query_until( qr/starting_resize/, q( @@ -86,22 +86,79 @@ sub run_injection_point_test # Detach injection point from injection session $injection_session->query_safe("SELECT injection_points_detach('$injection_point')",verbose => $verbose); - # Verify resize completed successfully - is($query_session->query_safe("SELECT current_setting('shared_buffers')", verbose => $verbose), $target_size, - "resize completed successfully to $target_size"); - - # Confirm the server is functional and the resize took effect. - my $result = $node->safe_psql('postgres', - "SELECT setting from pg_settings where name = 'shared_buffers'"); - # Check buffer pool size using pg_buffercache after resize completion is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", verbose => $verbose), - $result, "pg_buffercache COUNT(*) correct after $test_name ($operation_type)"); + $target_buffers, "pg_buffercache COUNT(*) correct after $test_name ($operation_type)"); # Wait for client to complete ok($client->quit, "client succeeded during $test_name ($operation_type)"); } +# Pause a pg_buffercache operation at the given injection point, resize the +# buffer pool while the operation is paused, then wake it up and verify that +# the server remains functional and the resize took effect. +sub run_resize_during_paused_operation +{ + my ($test_name, $injection_point, $operation_sql, $target_size, + $target_buffers, $operation_type) = @_; + + my $verbose = 0; + + note("Test $test_name ($operation_type)"); + + # Attach injection point + $injection_session->query_safe( + "SELECT injection_points_attach('$injection_point', 'wait')", + verbose => $verbose); + + # Start the operation in background - it will pause at injection point. + # Use on_error_stop => 0 so psql stays alive if the query errors out. + my $op_session = $node->background_psql('postgres', on_error_stop => 0); + $op_session->query_until( + qr/starting_op/, + qq( + \\echo starting_op + $operation_sql + ) + ); + + # Wait for the operation to reach the injection point + $query_session->wait_for_event('client backend', $injection_point, + verbose => $verbose); + + # Change shared_buffers to target size and reload config + $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); + $node->safe_psql('postgres', "SELECT pg_reload_conf()"); + + $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()", + verbose => $verbose); + + # Wake up the paused operation + $injection_session->query_safe( + "SELECT injection_points_wakeup('$injection_point')", + verbose => $verbose); + + # Collect the operation output + my $op_output = $op_session->query_safe(q(\echo 'done'), + verbose => $verbose); + note("operation stdout during $test_name ($operation_type): \n" + . $op_output); + note("operation stderr during $test_name ($operation_type): \n" + . ($op_session->{stderr} // '')); + $op_session->quit; + + # Detach injection point + $injection_session->query_safe( + "SELECT injection_points_detach('$injection_point')", + verbose => $verbose); + + # Verify the resize took effect and the server is functional + is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", + verbose => $verbose), + $target_buffers, + "pg_buffercache count matches after $test_name ($operation_type)"); +} + # Test injection points during buffer resize with client connections my @common_injection_tests = ( { @@ -122,10 +179,10 @@ sub run_injection_point_test foreach my $test (@common_injection_tests) { # Test shrinking scenario - run_injection_point_test($test->{name}, $test->{injection_point}, '272kB', 'shrinking'); + run_scan_during_paused_resize($test->{name}, $test->{injection_point}, '272kB', '34', 'shrinking'); # Test expanding scenario - run_injection_point_test($test->{name}, $test->{injection_point}, '400kB', 'expanding'); + run_scan_during_paused_resize($test->{name}, $test->{injection_point}, '400kB', '50', 'expanding'); } my @shrink_only_tests = ( @@ -137,111 +194,72 @@ sub run_injection_point_test ); foreach my $test (@shrink_only_tests) { - run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'shrinking only'); + run_scan_during_paused_resize($test->{name}, $test->{injection_point}, $test->{size}, '25', 'shrinking only'); } my @expand_only_tests = ( { name => 'expand barrier complete', injection_point => 'pgrsb-expand-barrier-sent', - size => '416kB', + size => '8MB', } ); foreach my $test (@expand_only_tests) { - run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'expanding only'); -} - -# Function to test buffercache scan behavior during resize operations. -sub run_buffercache_injection_test -{ - my ($test_name, $buffercache_injection_point, $target_size, $operation_type) = @_; - - my $verbose = 0; - - note("Test buffercache with $test_name ($operation_type)"); - - # Attach injection point at the buffercache scan - $injection_session->query_safe( - "SELECT injection_points_attach('$buffercache_injection_point', 'wait')", - verbose => $verbose); - - # Start buffercache query in background - it will pause at injection point. - # Use on_error_stop => 0 so psql stays alive if the query errors out. - my $buffercache_session = $node->background_psql('postgres', on_error_stop => 0); - $buffercache_session->query_until( - qr/starting_buffercache/, - q( - \echo starting_buffercache - SELECT COUNT(*) FROM pg_buffercache; - ) - ); - - # Wait for buffercache to reach injection point - $query_session->wait_for_event('client backend', $buffercache_injection_point, - verbose => $verbose); - - # Change shared_buffers to target size and reload config - $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); - $node->safe_psql('postgres', "SELECT pg_reload_conf()"); - - # Start a new background session for resize - $resize_session->query_safe("SELECT pg_resize_shared_buffers()", - verbose => $verbose); - $resize_session->quit; - - # Wake up buffercache scan and collect its results - $injection_session->query_safe( - "SELECT injection_points_wakeup('$buffercache_injection_point')", - verbose => $verbose); - - my ($buffercache_output, $buffercache_err); - eval { ($buffercache_output, $buffercache_err) = $buffercache_session->query(q(\echo 'done')); }; - my $query_err = $@; - - # Print the stderr/stdout output (even if query() died due to crash) - note("buffercache stderr during $test_name ($operation_type): \n" . ($buffercache_session->{stderr} // '')); - note("buffercache stdout during $test_name ($operation_type): \n" . ($buffercache_output // '')); - note("buffercache query error during $test_name ($operation_type): $query_err") if $query_err; - $buffercache_session->{stderr} = ''; - - eval { $buffercache_session->quit; }; - - # Detach injection point - eval { - $injection_session->query_safe( - "SELECT injection_points_detach('$buffercache_injection_point')", - verbose => $verbose); - }; - - # Confirm the server is functional and the resize took effect. - my $result = $node->safe_psql('postgres', "SELECT setting from pg_settings where name = 'shared_buffers'"); - - is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", - verbose => $verbose), - $result, - "pg_buffercache count matches after $test_name ($operation_type)"); + run_scan_during_paused_resize($test->{name}, $test->{injection_point}, $test->{size}, '1024', 'expanding only'); } # Test buffercache injection points - pausing buffercache while resize occurs my @buffercache_injection_tests = ( - { - name => 'before the buffer pool scan starts', - injection_point => 'pg-buffercache-scan-start', - }, # Basic fail where after buffer change there are valid buffers # { - # name => 'before getting buffer description - 2', - # injection_point => 'pg-buffercache-after-getdesc', - # }, # Failure where after buffer change there are no valid buffers; + # name => 'before the buffer pool scan starts', + # injection_point => 'pg-buffercache-scan-start', + # }, # Basic fail where after buffer change there are valid buffers + { + name => 'before getting buffer description - 2', + injection_point => 'pg-buffercache-after-getdesc', + }, # Failure where after buffer change there are no valid buffers; ); foreach my $test (@buffercache_injection_tests) { # Test with shrinking - run_buffercache_injection_test($test->{name}, $test->{injection_point}, '256kB', 'shrinking'); + run_resize_during_paused_operation($test->{name}, $test->{injection_point}, + 'SELECT COUNT(*) FROM pg_buffercache;', '256kB', '32', 'shrinking'); # Test with expanding - run_buffercache_injection_test($test->{name}, $test->{injection_point}, '384kB', 'expanding'); + run_resize_during_paused_operation($test->{name}, $test->{injection_point}, + 'SELECT COUNT(*) FROM pg_buffercache;', '384kB', '48', 'expanding'); +} + +# Test evict with resize - pausing evict while resize occurs. +# After shrinking, buffer 33 is beyond the new pool size. The evict read +# currentNBuffers (1024) before the shrink, so it considers 33 valid and +# attempts the evict on a buffer that no longer belongs to the pool. +# After expanding, buffer 1 is always valid. +my @evict_injection_tests = ( + { + name => 'evict invalid buffer after shrink', + injection_point => 'pg-buffercache-evict-before-check', + sql => 'SELECT pg_buffercache_evict(33);', + size => '256kB', + buffers => '32', + type => 'shrinking', + }, + { + name => 'evict valid buffer after expand', + injection_point => 'pg-buffercache-evict-before-check', + sql => 'SELECT pg_buffercache_evict(1);', + size => '384kB', + buffers => '48', + type => 'expanding', + }, +); + +foreach my $test (@evict_injection_tests) +{ + run_resize_during_paused_operation($test->{name}, $test->{injection_point}, + $test->{sql}, $test->{size}, $test->{buffers}, $test->{type}); } $injection_session->quit; From 46f5850646da4c4d46cd3cc916d09162b4f73e57 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Mon, 6 Apr 2026 14:48:18 +0000 Subject: [PATCH 8/9] Refactor pg_buffercache tests to improve readability and enhance injection point handling during buffer resizing --- contrib/pg_buffercache/pg_buffercache_pages.c | 1 + contrib/pg_buffercache/t/001_basic.pl | 83 ++++++++----------- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index 7197ef50eb355..8b05d97ea69c1 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -19,6 +19,7 @@ #include "utils/rel.h" #include "utils/tuplestore.h" + #define NUM_BUFFERCACHE_PAGES_MIN_ELEM 8 #define NUM_BUFFERCACHE_PAGES_ELEM 9 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5 diff --git a/contrib/pg_buffercache/t/001_basic.pl b/contrib/pg_buffercache/t/001_basic.pl index 8b00c5569982f..fd8b634e03935 100644 --- a/contrib/pg_buffercache/t/001_basic.pl +++ b/contrib/pg_buffercache/t/001_basic.pl @@ -43,16 +43,12 @@ # produce correct results. sub run_scan_during_paused_resize { - my ($test_name, $injection_point, $target_size, $target_buffers, $operation_type) = @_; + my ($test_name, $injection_point, $target_size, $target_buffers, + $operation_type) = @_; - # Silence the logging of the statements we run to avoid - # unnecessarily bloating the test logs. This runs before the - # upgrade we're testing, so the details should not be very - # interesting for debugging. But if needed, you can make it more - # verbose by setting this. my $verbose = 0; - note("Test with $test_name ($operation_type)"); + note("Test $test_name ($operation_type)"); # Update buffer pool size $resize_session->query_safe("ALTER SYSTEM SET shared_buffers = '$target_size'", verbose => $verbose); @@ -61,6 +57,7 @@ sub run_scan_during_paused_resize # Set up injection point in injection session $injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')", verbose => $verbose); + # Start the resize in background - it will pause at injection point $resize_session->query_until( qr/starting_resize/, q( @@ -69,32 +66,31 @@ sub run_scan_during_paused_resize ) ); - # Wait until resize actually reaches the injection point + # Wait until resize actually reaches the injection point using the query session $query_session->wait_for_event('client backend', $injection_point, verbose => $verbose); - # Start bufferscan while resize is paused + # Start a client while resize is paused my $client = $node->background_psql('postgres'); $client->query_safe("SELECT count(*) FROM pg_buffercache", verbose => $verbose); # Wake up the injection point from injection session $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')", verbose => $verbose); - # Wait for the resize operation to complete + # Wait for the resize operation to complete. $resize_session->query(q(\echo 'done'), verbose => $verbose); - $resize_session->quit; # Detach injection point from injection session - $injection_session->query_safe("SELECT injection_points_detach('$injection_point')",verbose => $verbose); + $injection_session->query_safe("SELECT injection_points_detach('$injection_point')", verbose => $verbose); # Check buffer pool size using pg_buffercache after resize completion is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", verbose => $verbose), - $target_buffers, "pg_buffercache COUNT(*) correct after $test_name ($operation_type)"); + $target_buffers, "pg_buffercache count matches after $test_name ($operation_type)"); # Wait for client to complete ok($client->quit, "client succeeded during $test_name ($operation_type)"); } -# Pause a pg_buffercache operation at the given injection point, resize the +# Pause a pg_buffercache operation(pg_buffercache_scan and pg_buffercache_evict) at the given injection point, resize the # buffer pool while the operation is paused, then wake it up and verify that # the server remains functional and the resize took effect. sub run_resize_during_paused_operation @@ -106,10 +102,8 @@ sub run_resize_during_paused_operation note("Test $test_name ($operation_type)"); - # Attach injection point - $injection_session->query_safe( - "SELECT injection_points_attach('$injection_point', 'wait')", - verbose => $verbose); + # Set up injection point in injection session + $injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')", verbose => $verbose); # Start the operation in background - it will pause at injection point. # Use on_error_stop => 0 so psql stays alive if the query errors out. @@ -122,41 +116,30 @@ sub run_resize_during_paused_operation ) ); - # Wait for the operation to reach the injection point - $query_session->wait_for_event('client backend', $injection_point, - verbose => $verbose); + # Wait until the operation actually reaches the injection point using the query session + $query_session->wait_for_event('client backend', $injection_point, verbose => $verbose); - # Change shared_buffers to target size and reload config + # Start a resize operation while the first operation is paused at injection point $node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_size'"); $node->safe_psql('postgres', "SELECT pg_reload_conf()"); - $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()", - verbose => $verbose); + $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()", verbose => $verbose); - # Wake up the paused operation - $injection_session->query_safe( - "SELECT injection_points_wakeup('$injection_point')", - verbose => $verbose); + # Wake up the injection point from injection session + $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')", verbose => $verbose); # Collect the operation output - my $op_output = $op_session->query_safe(q(\echo 'done'), - verbose => $verbose); - note("operation stdout during $test_name ($operation_type): \n" - . $op_output); - note("operation stderr during $test_name ($operation_type): \n" - . ($op_session->{stderr} // '')); + my $op_output = $op_session->query_safe(q(\echo 'done'), verbose => $verbose); + note("operation stdout during $test_name ($operation_type): \n" . $op_output); + note("operation stderr during $test_name ($operation_type): \n" . ($op_session->{stderr} // '')); $op_session->quit; - # Detach injection point - $injection_session->query_safe( - "SELECT injection_points_detach('$injection_point')", - verbose => $verbose); + # Detach injection point from injection session + $injection_session->query_safe("SELECT injection_points_detach('$injection_point')", verbose => $verbose); - # Verify the resize took effect and the server is functional - is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", - verbose => $verbose), - $target_buffers, - "pg_buffercache count matches after $test_name ($operation_type)"); + # Check buffer pool size using pg_buffercache after resize completion + is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", verbose => $verbose), + $target_buffers, "pg_buffercache count matches after $test_name ($operation_type)"); } # Test injection points during buffer resize with client connections @@ -211,14 +194,14 @@ sub run_resize_during_paused_operation # Test buffercache injection points - pausing buffercache while resize occurs my @buffercache_injection_tests = ( - # { - # name => 'before the buffer pool scan starts', - # injection_point => 'pg-buffercache-scan-start', - # }, # Basic fail where after buffer change there are valid buffers { - name => 'before getting buffer description - 2', - injection_point => 'pg-buffercache-after-getdesc', - }, # Failure where after buffer change there are no valid buffers; + name => 'before the buffer pool scan starts', + injection_point => 'pg-buffercache-scan-start', + }, # Basic fail where after buffer change there are valid buffers + # { + # name => 'before getting buffer description - 2', + # injection_point => 'pg-buffercache-after-getdesc', + # }, # Failure where after buffer change there are no valid buffers; ); foreach my $test (@buffercache_injection_tests) From aca1c53a4c851290e20d85f9f7e873b956eab107 Mon Sep 17 00:00:00 2001 From: Palak Chaturvedi Date: Wed, 3 Jun 2026 16:12:39 +0000 Subject: [PATCH 9/9] Address review: revert non-injection-point changes in pg_buffercache_pages.c Revert NBuffers -> currentNBuffers changes in pg_buffercache_os_pages_internal, pg_buffercache_summary, and pg_buffercache_usage_counts as requested by reviewer (comments #7, #15, #22). Only injection point additions and the coupled currentNBuffers change in pg_buffercache_evict are retained. Also fix typo 'curent' -> 'current' in TODO comment, restore missing newline at EOF, and improve test file: add scan count validation, use query_safe consistently, clean up session handling and comments. --- contrib/pg_buffercache/Makefile | 2 +- contrib/pg_buffercache/pg_buffercache_pages.c | 19 +++++------- contrib/pg_buffercache/t/001_basic.pl | 29 +++++++++++-------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile index c632d8042959d..bb7f8ed7dcc6e 100644 --- a/contrib/pg_buffercache/Makefile +++ b/contrib/pg_buffercache/Makefile @@ -13,7 +13,7 @@ DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \ PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time" REGRESS = pg_buffercache pg_buffercache_numa -TAP_TESTS = 1 +TAP_TESTS = 2 ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index 8b05d97ea69c1..04ce3f2011f00 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -216,7 +216,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) /* * TODO: We should just scan the entire buffer descriptor array - * instead of relying on curent buffer pool size. But that can + * instead of relying on current buffer pool size. But that can * happen if only we setup the descriptor array large enough at * the server startup time. */ @@ -366,7 +366,6 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) int max_entries; char *startptr, *endptr; - int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); /* If NUMA information is requested, initialize NUMA support. */ if (include_numa && pg_numa_init() == -1) @@ -409,7 +408,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) startptr = (char *) TYPEALIGN_DOWN(os_page_size, BufferGetBlock(1)); endptr = (char *) TYPEALIGN(os_page_size, - (char *) BufferGetBlock(currentNBuffers) + BLCKSZ); + (char *) BufferGetBlock(NBuffers) + BLCKSZ); os_page_count = (endptr - startptr) / os_page_size; /* Used to determine the NUMA node for all OS pages at once */ @@ -435,7 +434,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) Assert(idx == os_page_count); elog(DEBUG1, "NUMA: NBuffers=%d os_page_count=" UINT64_FORMAT " " - "os_page_size=%zu", currentNBuffers, os_page_count, os_page_size); + "os_page_size=%zu", NBuffers, os_page_count, os_page_size); /* * If we ever get 0xff back from kernel inquiry, then we probably @@ -484,7 +483,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) * without reallocating memory. */ pages_per_buffer = Max(1, BLCKSZ / os_page_size) + 1; - max_entries = currentNBuffers * pages_per_buffer; + max_entries = NBuffers * pages_per_buffer; /* Allocate entries for BufferCacheOsPagesRec records. */ fctx->record = (BufferCacheOsPagesRec *) @@ -507,7 +506,7 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) */ startptr = (char *) TYPEALIGN_DOWN(os_page_size, (char *) BufferGetBlock(1)); idx = 0; - for (i = 0; i < currentNBuffers; i++) + for (i = 0; i < NBuffers; i++) { char *buffptr = (char *) BufferGetBlock(i + 1); BufferDesc *bufHdr; @@ -649,12 +648,11 @@ pg_buffercache_summary(PG_FUNCTION_ARGS) int32 buffers_dirty = 0; int32 buffers_pinned = 0; int64 usagecount_total = 0; - int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); - for (int i = 0; i < currentNBuffers; i++) + for (int i = 0; i < NBuffers; i++) { BufferDesc *bufHdr; uint64 buf_state; @@ -712,11 +710,10 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS) int pinned[BM_MAX_USAGE_COUNT + 1] = {0}; Datum values[NUM_BUFFERCACHE_USAGE_COUNTS_ELEM]; bool nulls[NUM_BUFFERCACHE_USAGE_COUNTS_ELEM] = {0}; - int currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers); InitMaterializedSRF(fcinfo, 0); - for (int i = 0; i < currentNBuffers; i++) + for (int i = 0; i < NBuffers; i++) { BufferDesc *bufHdr = GetBufferDescriptor(i); uint64 buf_state = pg_atomic_read_u64(&bufHdr->state); @@ -1014,4 +1011,4 @@ pg_buffercache_lookup_table_entries(PG_FUNCTION_ARGS) BufTableGetContents(rsinfo->setResult, rsinfo->setDesc); return (Datum) 0; -} \ No newline at end of file +} diff --git a/contrib/pg_buffercache/t/001_basic.pl b/contrib/pg_buffercache/t/001_basic.pl index fd8b634e03935..7de4fa98f1a61 100644 --- a/contrib/pg_buffercache/t/001_basic.pl +++ b/contrib/pg_buffercache/t/001_basic.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025-2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group # # Test pg_buffercache scan behavior during shared_buffer resizing using # injection points. @@ -10,7 +10,7 @@ use Test::More; # Skip this test if injection points are not supported -if ($ENV{enable_injection_points} ne 'yes') +if (($ENV{enable_injection_points} // '') ne 'yes') { plan skip_all => 'Injection points not supported by this build'; } @@ -69,15 +69,16 @@ sub run_scan_during_paused_resize # Wait until resize actually reaches the injection point using the query session $query_session->wait_for_event('client backend', $injection_point, verbose => $verbose); - # Start a client while resize is paused + # Start a client while resize is paused and verify scan succeeds my $client = $node->background_psql('postgres'); - $client->query_safe("SELECT count(*) FROM pg_buffercache", verbose => $verbose); + my $client_count = $client->query_safe("SELECT count(*) FROM pg_buffercache", verbose => $verbose); + cmp_ok($client_count, '>', 0, "client scan returned rows during $test_name ($operation_type)"); # Wake up the injection point from injection session $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')", verbose => $verbose); # Wait for the resize operation to complete. - $resize_session->query(q(\echo 'done'), verbose => $verbose); + $resize_session->query_safe(q(\echo 'done'), verbose => $verbose); # Detach injection point from injection session $injection_session->query_safe("SELECT injection_points_detach('$injection_point')", verbose => $verbose); @@ -90,9 +91,10 @@ sub run_scan_during_paused_resize ok($client->quit, "client succeeded during $test_name ($operation_type)"); } -# Pause a pg_buffercache operation(pg_buffercache_scan and pg_buffercache_evict) at the given injection point, resize the -# buffer pool while the operation is paused, then wake it up and verify that -# the server remains functional and the resize took effect. +# Pause a pg_buffercache operation (pg_buffercache_scan and +# pg_buffercache_evict) at the given injection point, resize the buffer pool +# while the operation is paused, then wake it up and verify that the server +# remains functional and the resize took effect. sub run_resize_during_paused_operation { my ($test_name, $injection_point, $operation_sql, $target_size, @@ -128,11 +130,11 @@ sub run_resize_during_paused_operation # Wake up the injection point from injection session $injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')", verbose => $verbose); - # Collect the operation output + # Collect the operation output and verify session completed my $op_output = $op_session->query_safe(q(\echo 'done'), verbose => $verbose); note("operation stdout during $test_name ($operation_type): \n" . $op_output); note("operation stderr during $test_name ($operation_type): \n" . ($op_session->{stderr} // '')); - $op_session->quit; + ok($op_session->quit, "operation session completed during $test_name ($operation_type)"); # Detach injection point from injection session $injection_session->query_safe("SELECT injection_points_detach('$injection_point')", verbose => $verbose); @@ -198,10 +200,12 @@ sub run_resize_during_paused_operation name => 'before the buffer pool scan starts', injection_point => 'pg-buffercache-scan-start', }, # Basic fail where after buffer change there are valid buffers + # TODO: Enable once pg-buffercache-after-getdesc handles mid-scan + # descriptor invalidation correctly after a shrink. # { - # name => 'before getting buffer description - 2', + # name => 'before getting buffer description', # injection_point => 'pg-buffercache-after-getdesc', - # }, # Failure where after buffer change there are no valid buffers; + # }, ); foreach my $test (@buffercache_injection_tests) @@ -247,5 +251,6 @@ sub run_resize_during_paused_operation $injection_session->quit; $query_session->quit; +$resize_session->quit; done_testing(); \ No newline at end of file