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

---
 .../modules/test_aio/t/005_write_combining.pl | 120 ++++++++++++
 src/test/modules/test_aio/test_aio--1.0.sql   |  16 ++
 src/test/modules/test_aio/test_aio.c          | 178 ++++++++++++++++++
 3 files changed, 314 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 d9c66b3cb01..089b7094eda 100644
--- a/src/test/modules/test_aio/t/005_write_combining.pl
+++ b/src/test/modules/test_aio/t/005_write_combining.pl
@@ -26,9 +26,12 @@ synchronous_commit = off
 
 $node->start();
 
+$node->safe_psql('postgres', 'CREATE EXTENSION test_aio');
+
 my $block_size = $node->safe_psql('postgres',
 	"SELECT current_setting('block_size')::int");
 
+test_checkpointer_combines_writes($node, $block_size);
 test_copy_from_combines_writes($node, $block_size);
 
 $node->stop();
@@ -66,6 +69,73 @@ sub assert_combined_writes
 	ok($avg_write_bytes > $block_size, "$label combined writes");
 }
 
+sub assert_writes
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($node, $label, $backend_type, $context, $expected_writes, $expected_bytes) = @_;
+	my ($writes, $write_bytes, $avg_write_bytes) =
+	  io_stat_writes($node, $backend_type, $context);
+
+	note "$label: writes=$writes write_bytes=$write_bytes avg_write_bytes=$avg_write_bytes";
+
+	is($writes, $expected_writes, "$label write count");
+	is($write_bytes, $expected_bytes, "$label write bytes");
+}
+
+sub assert_writes_at_least
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($node, $label, $backend_type, $context, $expected_writes, $expected_bytes) = @_;
+	my ($writes, $write_bytes, $avg_write_bytes) =
+	  io_stat_writes($node, $backend_type, $context);
+
+	note "$label: writes=$writes write_bytes=$write_bytes avg_write_bytes=$avg_write_bytes";
+	ok($writes >= $expected_writes,
+		"$label wrote at least $expected_writes times");
+	ok($write_bytes >= $expected_bytes,
+		"$label wrote at least $expected_bytes bytes");
+}
+
+sub assert_blocks_dirty
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($node, $table, $blocks, $expected, $label) = @_;
+
+	is($node->safe_psql('postgres',
+		"SELECT true = ALL (rel_blocks_are_dirty('$table', ARRAY[$blocks]))"),
+		$expected, $label);
+}
+
+sub assert_any_blocks_dirty
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($node, $table, $blocks, $expected, $label) = @_;
+
+	is($node->safe_psql('postgres',
+		"SELECT true = ANY (rel_blocks_are_dirty('$table', ARRAY[$blocks]))"),
+		$expected, $label);
+}
+
+sub flush_and_reset_io_stats
+{
+	my ($node, $psql) = @_;
+
+	$psql->query_safe('SELECT pg_stat_force_next_flush()');
+	$node->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')");
+}
+
+sub dirty_blocks
+{
+	my ($psql, $table, $blocks) = @_;
+
+	$psql->query_safe(
+		"SELECT make_blocks_unused_dirty_flushed('$table', ARRAY[$blocks])");
+}
+
 sub test_copy_from_combines_writes
 {
 	my ($node, $block_size) = @_;
@@ -92,3 +162,53 @@ sub test_copy_from_combines_writes
 	is($node->safe_psql('postgres', "SELECT count(*) FROM wc_copy"),
 		$rows, 'copy from inserted rows');
 }
+
+sub test_checkpointer_combines_writes
+{
+	my ($node, $block_size) = @_;
+	my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+	$node->safe_psql(
+		'postgres', qq(
+	CREATE TABLE wc_checkpointer (id int, payload text);
+	INSERT INTO wc_checkpointer SELECT g, repeat('y', 200) FROM generate_series(1, 1000) AS g;
+	SELECT flush_rel_buffers('wc_checkpointer'::regclass);
+	CHECKPOINT;
+	));
+
+	####
+	# Test one big combined write by checkpointer.
+	####
+
+	dirty_blocks($psql, 'wc_checkpointer', '0,1,2,3,4,5');
+	assert_blocks_dirty($node, 'wc_checkpointer', '0,1,2,3,4,5', 't',
+		'contiguous buffers are dirty before checkpoint');
+
+	flush_and_reset_io_stats($node, $psql);
+	$node->safe_psql('postgres', 'CHECKPOINT');
+	$node->safe_psql('postgres', 'SELECT pg_stat_force_next_flush()');
+
+	assert_combined_writes($node, 'contiguous checkpointer', 'checkpointer',
+		'normal', $block_size);
+	assert_any_blocks_dirty($node, 'wc_checkpointer', '0,1,2,3,4,5', 'f',
+		'checkpointer 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_checkpointer', ARRAY[1,3,5])");
+	dirty_blocks($psql, 'wc_checkpointer', '0,2,4');
+	flush_and_reset_io_stats($node, $psql);
+	$node->safe_psql('postgres', 'CHECKPOINT');
+	$node->safe_psql('postgres', 'SELECT pg_stat_force_next_flush()');
+
+	assert_writes_at_least($node, 'nonresident gaps checkpointer', 'checkpointer',
+		'normal', 3, 3 * $block_size);
+	assert_any_blocks_dirty($node, 'wc_checkpointer', '0,2,4', 'f',
+		'checkpointer 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 762ac29512f..e1dc6a5ef10 100644
--- a/src/test/modules/test_aio/test_aio--1.0.sql
+++ b/src/test/modules/test_aio/test_aio--1.0.sql
@@ -37,10 +37,26 @@ CREATE FUNCTION evict_rel(rel regclass)
 RETURNS pg_catalog.void STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
 
+CREATE FUNCTION flush_rel_buffers(rel regclass)
+RETURNS pg_catalog.void STRICT
+AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION make_blocks_unused_dirty_flushed(rel regclass, blocks 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;
+
 CREATE FUNCTION invalidate_rel_block(rel regclass, blockno int)
 RETURNS pg_catalog.void STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
 
+CREATE FUNCTION invalidate_rel_blocks(rel regclass, blocks 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 35efba1a5e3..c062b9177a4 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -36,6 +36,7 @@
 #include "utils/builtins.h"
 #include "utils/injection_point.h"
 #include "utils/rel.h"
+#include "utils/resowner.h"
 #include "utils/tuplestore.h"
 #include "utils/wait_event.h"
 
@@ -84,6 +85,8 @@ static const ShmemCallbacks inj_io_shmem_callbacks = {
 	.attach_fn = test_aio_shmem_attach,
 };
 
+static bool clear_usage_count_if_unpinned(BufferDesc *desc);
+
 
 static PgAioHandle *last_handle;
 
@@ -537,6 +540,32 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+PG_FUNCTION_INFO_V1(invalidate_rel_blocks);
+Datum
+invalidate_rel_blocks(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	ArrayType  *blocksarray = PG_GETARG_ARRAYTYPE_P(1);
+	Relation	rel;
+	uint32	   *blocks;
+	int			nblocks;
+
+	if (ARR_NDIM(blocksarray) != 1 ||
+		ARR_HASNULL(blocksarray) ||
+		ARR_ELEMTYPE(blocksarray) != INT4OID)
+		elog(ERROR, "expected 1 dimensional int4 array");
+
+	blocks = (uint32 *) ARR_DATA_PTR(blocksarray);
+	nblocks = ARR_DIMS(blocksarray)[0];
+
+	rel = relation_open(relid, AccessExclusiveLock);
+	for (int i = 0; i < nblocks; i++)
+		invalidate_one_block(rel, MAIN_FORKNUM, blocks[i]);
+	relation_close(rel, AccessExclusiveLock);
+
+	PG_RETURN_VOID();
+}
+
 PG_FUNCTION_INFO_V1(evict_rel);
 Datum
 evict_rel(PG_FUNCTION_ARGS)
@@ -586,6 +615,155 @@ evict_rel(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static bool
+clear_usage_count_if_unpinned(BufferDesc *desc)
+{
+	uint64		buf_state;
+
+	buf_state = LockBufHdr(desc);
+	for (;;)
+	{
+		uint64		new_buf_state;
+
+		if ((buf_state & BM_VALID) == 0 ||
+			BUF_STATE_GET_REFCOUNT(buf_state) != 0)
+		{
+			UnlockBufHdr(desc);
+			return false;
+		}
+
+		new_buf_state = buf_state;
+		new_buf_state &= ~BUF_USAGECOUNT_MASK;
+		new_buf_state &= ~BM_LOCKED;
+
+		if (pg_atomic_compare_exchange_u64(&desc->state, &buf_state,
+										   new_buf_state))
+			return true;
+	}
+}
+
+PG_FUNCTION_INFO_V1(flush_rel_buffers);
+Datum
+flush_rel_buffers(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Relation	rel;
+
+	rel = relation_open(relid, AccessExclusiveLock);
+	FlushRelationBuffers(rel);
+	relation_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(make_blocks_unused_dirty_flushed);
+Datum
+make_blocks_unused_dirty_flushed(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	ArrayType  *blocksarray = PG_GETARG_ARRAYTYPE_P(1);
+	Relation	rel;
+	uint32	   *blocks;
+	int			nblocks;
+
+	if (ARR_NDIM(blocksarray) != 1 ||
+		ARR_HASNULL(blocksarray) ||
+		ARR_ELEMTYPE(blocksarray) != INT4OID)
+		elog(ERROR, "expected 1 dimensional int4 array");
+
+	blocks = (uint32 *) ARR_DATA_PTR(blocksarray);
+	nblocks = ARR_DIMS(blocksarray)[0];
+
+	rel = relation_open(relid, AccessExclusiveLock);
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				errmsg("this function doesn't prepare local buffers"));
+
+	for (int i = 0; i < nblocks; i++)
+	{
+		Buffer		buf;
+		BufferDesc *desc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blocks[i], RBM_NORMAL, NULL);
+		desc = GetBufferDescriptor(buf - 1);
+		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+		FlushOneBuffer(buf);
+		MarkBufferDirty(buf);
+		UnlockReleaseBuffer(buf);
+
+		/*
+		 * There's no way to explicitly guarantee the usage count stays 0 for
+		 * our bgwriter tests. It requires careful coding and setup in the
+		 * test to make sure that nothing else is going to look for a free
+		 * buffer.
+		 */
+		if (!clear_usage_count_if_unpinned(desc))
+			elog(ERROR, "could not clear usage count for block %u",
+				 blocks[i]);
+	}
+
+	relation_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(rel_blocks_are_dirty);
+Datum
+rel_blocks_are_dirty(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	ArrayType  *blocksarray = PG_GETARG_ARRAYTYPE_P(1);
+	Relation	rel;
+	uint32	   *blocks;
+	int			nblocks;
+	Datum	   *dirty_datums;
+	ArrayType  *dirty_array;
+
+	if (ARR_NDIM(blocksarray) != 1 ||
+		ARR_HASNULL(blocksarray) ||
+		ARR_ELEMTYPE(blocksarray) != INT4OID)
+		elog(ERROR, "expected 1 dimensional int4 array");
+
+	blocks = (uint32 *) ARR_DATA_PTR(blocksarray);
+	nblocks = ARR_DIMS(blocksarray)[0];
+	dirty_datums = palloc0(sizeof(Datum) * nblocks);
+
+	rel = relation_open(relid, AccessShareLock);
+	for (int i = 0; i < nblocks; i++)
+	{
+		BufferTag	tag;
+		uint32		hash;
+		LWLock	   *partition_lock;
+		int			buf_id;
+		bool		is_dirty = false;
+
+		InitBufferTag(&tag, &rel->rd_locator, MAIN_FORKNUM, blocks[i]);
+		hash = BufTableHashCode(&tag);
+		partition_lock = BufMappingPartitionLock(hash);
+
+		LWLockAcquire(partition_lock, LW_SHARED);
+		buf_id = BufTableLookup(&tag, hash);
+		LWLockRelease(partition_lock);
+
+		if (buf_id >= 0)
+		{
+			BufferDesc *desc = GetBufferDescriptor(buf_id);
+
+			is_dirty = (pg_atomic_read_u64(&desc->state) & BM_DIRTY) != 0;
+		}
+
+		dirty_datums[i] = BoolGetDatum(is_dirty);
+	}
+	relation_close(rel, NoLock);
+
+	dirty_array = construct_array(dirty_datums, nblocks, BOOLOID, 1, true,
+								  TYPALIGN_CHAR);
+
+	PG_RETURN_ARRAYTYPE_P(dirty_array);
+}
+
 PG_FUNCTION_INFO_V1(buffer_create_toy);
 Datum
 buffer_create_toy(PG_FUNCTION_ARGS)
-- 
2.43.0

