From 994058c61e14b768e83e90337073fa215efba837 Mon Sep 17 00:00:00 2001 From: Melanie Plageman 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