From 95b497e3047b7bf3a72f745e5bc73a47ab13b0a0 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Sun, 13 Sep 2026 23:45:27 +0000 Subject: [PATCH v9 4/5] Test reading WAL from buffers across a segment boundary. The previous two commits let more WAL readers use WALReadFromBuffers() and close the open segment after a read that comes entirely from the WAL buffers and crosses into a new segment. This commit adds a test for that. A new injection point, wal-read-from-buffers-force-miss, forces a buffer miss for reads that do not start at a segment boundary. With it attached, the first page of a new segment is read from the WAL buffers while the next page falls back to the file, which reproduces the stale open segment unless it is closed after the read that came from the WAL buffers. The test checks this for both pg_walinspect and a logical walsender. Author: Bharath Rupireddy Reviewed-by: Kirill Reshke Discussion: https://www.postgresql.org/message-id/CALj2ACVfF2Uj9NoFy-5m98HNtjHpuD17EDE9twVeJng-jTAe7A%40mail.gmail.com --- src/backend/access/transam/xlog.c | 10 +++ src/test/subscription/Makefile | 2 + src/test/subscription/meson.build | 1 + .../subscription/t/099_wal_buffers_read.pl | 70 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 src/test/subscription/t/099_wal_buffers_read.pl diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 26f67dbfcfc..0c264330336 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -1831,6 +1831,16 @@ WALReadFromBuffers(char *dstbuf, XLogRecPtr startptr, Size count, if (RecoveryInProgress() || tli != GetWALInsertionTimeLine()) return 0; + /* + * Force a buffer miss for reads not starting at a segment boundary. See + * logical_read_xlog_page() for details. + */ +#ifdef USE_INJECTION_POINTS + if (XLogSegmentOffset(startptr, wal_segment_size) != 0 && + IS_INJECTION_POINT_ATTACHED("wal-read-from-buffers-force-miss")) + return 0; +#endif + Assert(XLogRecPtrIsValid(startptr)); /* diff --git a/src/test/subscription/Makefile b/src/test/subscription/Makefile index 1b22703dc21..01306dbd378 100644 --- a/src/test/subscription/Makefile +++ b/src/test/subscription/Makefile @@ -14,6 +14,8 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global EXTRA_INSTALL = contrib/hstore \ + contrib/pg_walinspect \ + contrib/test_decoding \ src/test/modules/injection_points export with_icu diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build index e71e95c6297..82bb3cad962 100644 --- a/src/test/subscription/meson.build +++ b/src/test/subscription/meson.build @@ -48,6 +48,7 @@ tests += { 't/036_sequences.pl', 't/037_except.pl', 't/038_walsnd_shutdown_timeout.pl', + 't/099_wal_buffers_read.pl', 't/100_bugs.pl', ], }, diff --git a/src/test/subscription/t/099_wal_buffers_read.pl b/src/test/subscription/t/099_wal_buffers_read.pl new file mode 100644 index 00000000000..c1392bf1929 --- /dev/null +++ b/src/test/subscription/t/099_wal_buffers_read.pl @@ -0,0 +1,70 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# A WAL read that comes entirely from the WAL buffers must not leave a stale +# open segment behind for a later file read. Exercised for +# read_local_xlog_page() (via pg_walinspect) and the logical walsender (via +# pg_recvlogical). + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(allows_streaming => 'logical'); +# Keep recent WAL in buffers so the new segment's first page is read from +# buffers rather than from a file, and keep the WAL writer asleep so that its +# opportunistic buffer pre-initialization does not evict that page meanwhile. +$node->append_conf( + 'postgresql.conf', qq(wal_buffers = 64MB +wal_writer_delay = 10s)); +$node->start; + +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points'); +$node->safe_psql('postgres', 'CREATE EXTENSION pg_walinspect'); +$node->safe_psql('postgres', + "SELECT pg_create_logical_replication_slot('slot', 'test_decoding')"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('wal-read-from-buffers-force-miss', + 'notice')"); + +# Emit one message per WAL page in a segment, plus a few more, so that the WAL +# written after the switch crosses a segment boundary by a couple of pages. +my $seg_size = $node->safe_psql('postgres', + "SELECT pg_size_bytes(current_setting('wal_segment_size'))"); +$node->safe_psql('postgres', 'SELECT pg_switch_wal()'); +my $start_lsn = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); +$node->safe_psql('postgres', + "SELECT count(pg_logical_emit_message(false, 'test', repeat('x', 8192))) + FROM generate_series(1, $seg_size / 8192 + 16)"); +my $end_lsn = $node->safe_psql('postgres', + "SELECT pg_logical_emit_message(false, 'test', 'flush', true)"); + +# read_local_xlog_page() path. +my ($ret, $stdout, $stderr) = $node->psql('postgres', + "SELECT count(*) > 0 FROM pg_get_wal_records_info('$start_lsn', '$end_lsn')"); +is($ret, 0, 'pg_walinspect reads across a segment boundary'); +is($stdout, 't', 'pg_walinspect returns records across the boundary'); + +# logical walsender path. +my ($rc, $rout, $rerr) = $node->pg_recvlogical_upto('postgres', 'slot', + $end_lsn, $PostgreSQL::Test::Utils::timeout_default); +is($rc, 0, 'walsender decodes across a segment boundary'); +unlike($rerr, qr/unexpected pageaddr/, + 'walsender did not reuse a stale segment'); + +$node->safe_psql('postgres', + "SELECT injection_points_detach('wal-read-from-buffers-force-miss')"); + +done_testing(); -- 2.47.3