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

---
 .../modules/test_aio/t/005_write_combining.pl | 100 ++++++++++++++++++
 src/test/modules/test_aio/test_aio--1.0.sql   |  12 +++
 src/test/modules/test_aio/test_aio.c          |  54 ++++++++++
 3 files changed, 166 insertions(+)

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 c51d14691e7..3c9b72925fc 100644
--- a/src/test/modules/test_aio/t/005_write_combining.pl
+++ b/src/test/modules/test_aio/t/005_write_combining.pl
@@ -34,6 +34,7 @@ my $block_size = $node->safe_psql('postgres',
 test_checkpointer_combines_writes($node, $block_size);
 test_regular_backend_combines_writes($node, $block_size);
 test_eager_clean_combines_writes($node, $block_size);
+test_bgwriter_combines_writes($node, $block_size);
 test_copy_from_combines_writes($node, $block_size);
 
 $node->stop();
@@ -164,6 +165,14 @@ sub dirty_blocks
 		"SELECT make_blocks_unused_dirty_flushed('$table', ARRAY[$blocks])");
 }
 
+sub run_bgwriter_cleaner
+{
+	my $psql = shift;
+
+	$psql->query_safe('SELECT run_bgwriter_cleaner(1000)');
+	$psql->query_safe('SELECT pg_stat_force_next_flush()');
+}
+
 sub allocate_until_blocks_clean
 {
 	my ($node, $psql, $table, $blocks, $filler, $label) = @_;
@@ -378,3 +387,94 @@ sub test_eager_clean_combines_writes
 
 	$psql->quit();
 }
+
+sub test_bgwriter_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');
+
+	# We create a relation and then flush all of its data. We can then mark
+	# buffers dirty for bgwriter without requiring WAL flush and thus preventing
+	# opportunistic write combining.
+	$node->safe_psql(
+		'postgres', qq(
+	CREATE TABLE wc_bgwriter (id int, payload text);
+	INSERT INTO wc_bgwriter SELECT g, repeat('y', 200) FROM generate_series(1, 1000) AS g;
+	SELECT flush_rel_buffers('wc_bgwriter'::regclass);
+	));
+
+	####
+	# Test one big combined write
+	####
+
+	# Mark blocks 0-5 dirty
+	dirty_blocks($psql, 'wc_bgwriter', '0,1,2,3,4,5');
+	assert_blocks_dirty($node, 'wc_bgwriter', '0,1,2,3,4,5', 't',
+		'contiguous buffers are dirty before bgwriter cleaner');
+
+	flush_and_reset_io_stats($node, $psql);
+	run_bgwriter_cleaner($psql);
+	# Should have written one big write
+	assert_writes($node, 'contiguous bgwriter cleaner', 'client backend',
+		'normal', 1, 6 * $block_size);
+	# None of those blocks should still be dirty
+	assert_any_blocks_dirty($node, 'wc_bgwriter', '0,1,2,3,4,5', 'f',
+		'bgwriter cleaner wrote contiguous dirty buffers');
+
+	####
+	# Test multiple single block writes when interspersed blocks are not in
+	# shared buffers
+	####
+
+	# Evict every other block and mark the other blocks dirty
+	$psql->query_safe(
+		"SELECT invalidate_rel_blocks('wc_bgwriter', ARRAY[1,3,5])");
+	dirty_blocks($psql, 'wc_bgwriter', '0,2,4');
+	flush_and_reset_io_stats($node, $psql);
+
+	run_bgwriter_cleaner($psql);
+	# The three blocks that were dirty should have been written out in three
+	# writes.
+	assert_writes($node, 'nonresident gaps bgwriter cleaner', 'client backend',
+		'normal', 3,
+		3 * $block_size);
+	# And none of them should be dirty anymore
+	assert_any_blocks_dirty($node, 'wc_bgwriter', '0,2,4', 'f',
+		'bgwriter cleaner wrote dirty buffers separated by nonresident gaps');
+
+	####
+	# Test two combined writes split around a pinned buffer
+	####
+
+	# Make sure first six blocks are all read in and marked dirty
+	dirty_blocks($psql, 'wc_bgwriter', '0,1,2,3,4,5');
+	flush_and_reset_io_stats($node, $psql);
+
+	# Do this in a transaction so that we can hold the buffer pin across
+	# multiple SQL statements by transferring ownership to the top transaction
+	# resource owner.
+	$psql->query_safe("BEGIN");
+	# Pin a block in the middle of the blocks
+	my $pinned_buf = $psql->query_safe(
+		"SELECT pin_rel_block('wc_bgwriter', 3)");
+
+	run_bgwriter_cleaner($psql);
+	# All the blocks should be in clean buffers except block 3 which was pinned
+	# and should still be marked dirty.
+	assert_any_blocks_dirty($node, 'wc_bgwriter', '0,1,2,4,5', 'f',
+		'bgwriter cleaner wrote buffers around pinned gap');
+	assert_blocks_dirty($node, 'wc_bgwriter', '3', 't',
+		'bgwriter cleaner skipped pinned buffer');
+	$psql->query_safe("SELECT release_buffer($pinned_buf)");
+	$psql->query_safe("COMMIT");
+	$psql->query_safe('SELECT pg_stat_force_next_flush()');
+	# Should have written out blocks 0,1,2 in one write and blocks 4 and 5 in
+	# another, totaling two writes.
+	assert_writes($node, 'pinned gap bgwriter cleaner', 'client backend',
+		'normal', 2, 5 * $block_size);
+
+	$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 d7d5006b3d5..6454328cd19 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 run_bgwriter_cleaner(lru_maxpages int4)
+RETURNS pg_catalog.int4 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;
@@ -61,6 +65,14 @@ CREATE FUNCTION invalidate_rel_blocks(rel regclass, blocks int4[])
 RETURNS pg_catalog.void STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
 
+CREATE FUNCTION pin_rel_block(rel regclass, blockno int4)
+RETURNS pg_catalog.int4 STRICT
+AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION release_buffer(buffer int4)
+RETURNS pg_catalog.void STRICT
+AS 'MODULE_PATHNAME' LANGUAGE C;
+
 CREATE FUNCTION buffer_create_toy(rel regclass, blockno int4)
 RETURNS pg_catalog.int4 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 b54764dcb46..67686a98eaf 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -709,6 +709,29 @@ make_blocks_unused_dirty_flushed(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+PG_FUNCTION_INFO_V1(run_bgwriter_cleaner);
+Datum
+run_bgwriter_cleaner(PG_FUNCTION_ARGS)
+{
+	int			lru_maxpages = PG_GETARG_INT32(0);
+	WritebackContext wb_context;
+	int			next_to_clean = 0;
+	uint32		next_passes = 0;
+	int			num_to_scan = NBuffers;
+	int			reusable_buffers = 0;
+	bool		maxwritten_clean;
+	int			num_written;
+
+	WritebackContextInit(&wb_context, &bgwriter_flush_after);
+	num_written = BgBufferSyncCleanBuffers(lru_maxpages, &wb_context,
+										   &next_to_clean, &next_passes,
+										   &num_to_scan, &reusable_buffers,
+										   NBuffers, &maxwritten_clean);
+	IssuePendingWritebacks(&wb_context, IOCONTEXT_NORMAL);
+
+	PG_RETURN_INT32(num_written);
+}
+
 PG_FUNCTION_INFO_V1(eager_clean_rel_block);
 Datum
 eager_clean_rel_block(PG_FUNCTION_ARGS)
@@ -789,6 +812,37 @@ rel_blocks_are_dirty(PG_FUNCTION_ARGS)
 	PG_RETURN_ARRAYTYPE_P(dirty_array);
 }
 
+PG_FUNCTION_INFO_V1(pin_rel_block);
+Datum
+pin_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);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, NULL);
+	ResourceOwnerForgetBuffer(CurrentResourceOwner, buf);
+	ResourceOwnerRememberBuffer(TopTransactionResourceOwner, buf);
+	relation_close(rel, NoLock);
+
+	PG_RETURN_INT32(buf);
+}
+
+PG_FUNCTION_INFO_V1(release_buffer);
+Datum
+release_buffer(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	ResourceOwnerForgetBuffer(TopTransactionResourceOwner, buf);
+	ResourceOwnerRememberBuffer(CurrentResourceOwner, buf);
+	ReleaseBuffer(buf);
+
+	PG_RETURN_VOID();
+}
+
 PG_FUNCTION_INFO_V1(buffer_create_toy);
 Datum
 buffer_create_toy(PG_FUNCTION_ARGS)
-- 
2.43.0

