From 69c1d5e90b0dba53ef7464000e6f82a15fdd50f7 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Sat, 5 Sep 2026 23:22:33 +0000
Subject: [PATCH v7 3/3] 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
fully satisfied from WAL buffers that 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
WAL buffers while the next page falls back to the file, which
reproduces the stale open segment unless it is closed after the
buffer-only read. The test checks this for both pg_walinspect and
a logical walsender.

Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
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    | 67 +++++++++++++++++++
 4 files changed, 80 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 2e3f177100b..862d8966438 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -1805,6 +1805,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..9ffe6ec9cc0
--- /dev/null
+++ b/src/test/subscription/t/099_wal_buffers_read.pl
@@ -0,0 +1,67 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# A WAL read served from 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

