From 17a6c723757b22ef5314045184fb78a2af324793 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Wed, 3 Jun 2026 18:19:35 -0400
Subject: [PATCH v15 16/19] Add client backend write combining test

---
 src/backend/storage/buffer/bufmgr.c           |   2 +-
 src/include/storage/buf_internals.h           |   3 +
 .../modules/test_aio/t/005_write_combining.pl | 166 ++++++++++++++++++
 src/test/modules/test_aio/test_aio--1.0.sql   |   4 +
 src/test/modules/test_aio/test_aio.c          |  25 +++
 5 files changed, 199 insertions(+), 1 deletion(-)

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index abe78c39306..09205bda26f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2708,7 +2708,7 @@ EagerCleanStrategyBuffer(BufferAccessStrategy strategy, Buffer bufnum,
  * with it in a single write. The victim buffer must be already pinned and
  * locked and will remain pinned upon return.
  */
-static void
+void
 EagerCleanBuffer(Buffer bufnum, BufferDesc *buf_hdr, IOContext io_context,
 				 WritebackContext *wb_context)
 {
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 70b22419ad4..d107c1c0215 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -587,6 +587,9 @@ extern StartBufferIOResult StartSharedBufferIO(BufferDesc *buf, bool forInput, b
 extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag_bits,
 							  bool forget_owner, bool release_aio);
 
+extern void EagerCleanBuffer(Buffer bufnum, BufferDesc *buf_hdr, IOContext io_context,
+							 WritebackContext *wb_context);
+
 
 /* freelist.c */
 extern IOContext IOContextForStrategy(BufferAccessStrategy strategy);
diff --git a/src/test/modules/test_aio/t/005_write_combining.pl b/src/test/modules/test_aio/t/005_write_combining.pl
index 089b7094eda..c51d14691e7 100644
--- a/src/test/modules/test_aio/t/005_write_combining.pl
+++ b/src/test/modules/test_aio/t/005_write_combining.pl
@@ -32,6 +32,8 @@ my $block_size = $node->safe_psql('postgres',
 	"SELECT current_setting('block_size')::int");
 
 test_checkpointer_combines_writes($node, $block_size);
+test_regular_backend_combines_writes($node, $block_size);
+test_eager_clean_combines_writes($node, $block_size);
 test_copy_from_combines_writes($node, $block_size);
 
 $node->stop();
@@ -56,6 +58,20 @@ WHERE backend_type = '$backend_type'
 	return split /\|/, $result;
 }
 
+sub io_stat_evictions
+{
+	my ($node, $backend_type, $context) = @_;
+
+	return $node->safe_psql(
+		'postgres', qq(
+SELECT COALESCE(sum(evictions), 0)::bigint
+FROM pg_stat_io
+WHERE backend_type = '$backend_type'
+  AND object = 'relation'
+  AND context = '$context';
+));
+}
+
 sub assert_combined_writes
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
@@ -98,6 +114,18 @@ sub assert_writes_at_least
 		"$label wrote at least $expected_bytes bytes");
 }
 
+sub assert_evictions_at_least
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($node, $label, $expected_evictions) = @_;
+	my $evictions = io_stat_evictions($node, 'client backend', 'normal');
+
+	note "$label: evictions=$evictions";
+	ok($evictions >= $expected_evictions,
+		"$label evicted at least $expected_evictions buffers");
+}
+
 sub assert_blocks_dirty
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
@@ -136,6 +164,33 @@ sub dirty_blocks
 		"SELECT make_blocks_unused_dirty_flushed('$table', ARRAY[$blocks])");
 }
 
+sub allocate_until_blocks_clean
+{
+	my ($node, $psql, $table, $blocks, $filler, $label) = @_;
+	my $shared_buffers_blocks = $node->safe_psql(
+		'postgres',
+		"SELECT pg_size_bytes(current_setting('shared_buffers')) / current_setting('block_size')::int"
+	);
+	my $extend_by = 1024;
+	my $allocated = 0;
+	my $max_allocations = 3 * $shared_buffers_blocks;
+
+	while ($node->safe_psql('postgres',
+			"SELECT true = ANY (rel_blocks_are_dirty('$table', ARRAY[$blocks]))") eq 't')
+	{
+		if ($allocated >= $max_allocations)
+		{
+			die "$label: blocks $blocks still dirty after $allocated buffer allocations";
+		}
+
+		$psql->query_safe("SELECT grow_rel('$filler'::regclass, $extend_by)");
+		$allocated += $extend_by;
+	}
+
+	note "$label: allocated $allocated buffers to reach regular backend victims";
+	$psql->query_safe('SELECT pg_stat_force_next_flush()');
+}
+
 sub test_copy_from_combines_writes
 {
 	my ($node, $block_size) = @_;
@@ -212,3 +267,114 @@ sub test_checkpointer_combines_writes
 
 	$psql->quit();
 }
+
+# A higher level test that might be too expensive to commit
+sub test_regular_backend_combines_writes
+{
+	my ($node, $block_size) = @_;
+	my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+	# Keep unrelated dirty buffers out of the client-backend write statistics.
+	$node->safe_psql('postgres', 'CHECKPOINT');
+
+	$node->safe_psql(
+		'postgres', qq(
+	CREATE TABLE wc_backend (id int, payload text);
+	INSERT INTO wc_backend SELECT g, repeat('y', 200) FROM generate_series(1, 1000) AS g;
+	SELECT flush_rel_buffers('wc_backend'::regclass);
+	CREATE UNLOGGED TABLE wc_backend_filler (id int);
+	CHECKPOINT;
+	));
+
+	####
+	# Test one big combined write from regular backend buffer allocation.
+	####
+
+	dirty_blocks($psql, 'wc_backend', '0,1,2,3,4,5');
+	assert_blocks_dirty($node, 'wc_backend', '0,1,2,3,4,5', 't',
+		'contiguous buffers are dirty before regular backend allocation');
+
+	flush_and_reset_io_stats($node, $psql);
+	allocate_until_blocks_clean($node, $psql, 'wc_backend', '0,1,2,3,4,5',
+		'wc_backend_filler', 'contiguous regular backend');
+
+	assert_writes($node, 'contiguous regular backend', 'client backend',
+		'normal', 1, 6 * $block_size);
+	assert_evictions_at_least($node, 'contiguous regular backend', 6);
+	assert_any_blocks_dirty($node, 'wc_backend', '0,1,2,3,4,5', 'f',
+		'regular backend wrote contiguous dirty buffers');
+
+	####
+	# Test multiple single block writes when interspersed blocks are not in
+	# shared buffers.
+	####
+
+	$psql->query_safe(
+		"SELECT invalidate_rel_blocks('wc_backend', ARRAY[1,3,5])");
+	dirty_blocks($psql, 'wc_backend', '0,2,4');
+	flush_and_reset_io_stats($node, $psql);
+
+	allocate_until_blocks_clean($node, $psql, 'wc_backend', '0,2,4',
+		'wc_backend_filler', 'nonresident gaps regular backend');
+	assert_writes($node, 'nonresident gaps regular backend', 'client backend',
+		'normal', 3,
+		3 * $block_size);
+	assert_evictions_at_least($node, 'nonresident gaps regular backend', 3);
+	assert_any_blocks_dirty($node, 'wc_backend', '0,2,4', 'f',
+		'regular backend wrote dirty buffers separated by nonresident gaps');
+
+	$psql->quit();
+}
+
+sub test_eager_clean_combines_writes
+{
+	my ($node, $block_size) = @_;
+	my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+	$node->safe_psql(
+		'postgres', qq(
+	CREATE TABLE wc_victim (id int, payload text);
+	INSERT INTO wc_victim SELECT g, repeat('y', 200) FROM generate_series(1, 1000) AS g;
+	SELECT flush_rel_buffers('wc_victim'::regclass);
+	CHECKPOINT;
+	));
+
+	####
+	# Test one big combined write when EagerCleanBuffer() is called directly.
+	####
+
+	dirty_blocks($psql, 'wc_victim', '0,1,2,3,4,5');
+	assert_blocks_dirty($node, 'wc_victim', '0,1,2,3,4,5', 't',
+		'contiguous buffers are dirty before direct eager clean');
+
+	flush_and_reset_io_stats($node, $psql);
+	$psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 0)");
+	$psql->query_safe('SELECT pg_stat_force_next_flush()');
+
+	assert_writes($node, 'contiguous direct eager clean', 'client backend',
+		'normal', 1, 6 * $block_size);
+	assert_any_blocks_dirty($node, 'wc_victim', '0,1,2,3,4,5', 'f',
+		'direct eager clean wrote contiguous dirty buffers');
+
+	####
+	# Test multiple single block writes when interspersed blocks are not in
+	# shared buffers.
+	####
+
+	$psql->query_safe(
+		"SELECT invalidate_rel_blocks('wc_victim', ARRAY[1,3,5])");
+	dirty_blocks($psql, 'wc_victim', '0,2,4');
+	flush_and_reset_io_stats($node, $psql);
+
+	$psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 0)");
+	$psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 2)");
+	$psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 4)");
+	$psql->query_safe('SELECT pg_stat_force_next_flush()');
+	assert_writes($node, 'nonresident gaps direct eager clean', 'client backend',
+		'normal', 3,
+		3 * $block_size);
+	assert_any_blocks_dirty($node, 'wc_victim', '0,2,4', 'f',
+		'direct eager clean wrote dirty buffers separated by nonresident gaps');
+
+	$psql->quit();
+}
diff --git a/src/test/modules/test_aio/test_aio--1.0.sql b/src/test/modules/test_aio/test_aio--1.0.sql
index e1dc6a5ef10..d7d5006b3d5 100644
--- a/src/test/modules/test_aio/test_aio--1.0.sql
+++ b/src/test/modules/test_aio/test_aio--1.0.sql
@@ -45,6 +45,10 @@ CREATE FUNCTION make_blocks_unused_dirty_flushed(rel regclass, blocks int4[])
 RETURNS pg_catalog.void STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
 
+CREATE FUNCTION eager_clean_rel_block(rel regclass, blockno int4)
+RETURNS pg_catalog.void STRICT
+AS 'MODULE_PATHNAME' LANGUAGE C;
+
 CREATE FUNCTION rel_blocks_are_dirty(rel regclass, blocks int4[])
 RETURNS pg_catalog.bool[] STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index c062b9177a4..b54764dcb46 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -709,6 +709,31 @@ make_blocks_unused_dirty_flushed(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+PG_FUNCTION_INFO_V1(eager_clean_rel_block);
+Datum
+eager_clean_rel_block(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	BlockNumber blkno = PG_GETARG_UINT32(1);
+	Relation	rel;
+	Buffer		buf;
+
+	rel = relation_open(relid, AccessShareLock);
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errmsg("cannot eager clean local buffers")));
+
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, NULL);
+	LockBuffer(buf, BUFFER_LOCK_SHARE_EXCLUSIVE);
+	EagerCleanBuffer(buf, GetBufferDescriptor(buf - 1), IOCONTEXT_NORMAL,
+					 &BackendWritebackContext);
+	ReleaseBuffer(buf);
+
+	relation_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
 PG_FUNCTION_INFO_V1(rel_blocks_are_dirty);
 Datum
 rel_blocks_are_dirty(PG_FUNCTION_ARGS)
-- 
2.43.0

