On Mon, Oct 19, 2026, Bharath Rupireddy
<[email protected]> wrote:
> I looked at the TAP test, and I found it a bit hard to follow given
> the many hard-coded values, [...]
> Would something like the below work instead [1]?

Yes, your test is easier to follow, so v7 uses it as 0002 in place of
the test_misc one.  0001 is unchanged from v6.

With 0001, the test passed 5 runs out of 5 on master.  Without it, all
3 runs failed, with pg_switch_wal() returning the boundary + 40.  Since
data checksums are now on by default, I also checked with pg_waldump
that no hint-bit FPI lands in the segment the test fills: it holds only
the 17 logical messages and the switch record.

The test applies to REL_15 through REL_19 as well, with only an offset.

Regards,
Paul Kim
From 25dc8e81732c2ef3964fc7117ae00121fab260a7 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <[email protected]>
Date: Fri, 9 Oct 2026 04:16:25 +0000
Subject: [PATCH v7 1/2] Fix end LSN reported for an xlog-switch record ending
 on a page boundary

A page header size is added to the end of an xlog-switch record
even when the record ends exactly on a page boundary and crosses
no header. At a segment boundary that puts the end past the end of
reserved WAL. The position then goes into the asynchronous commit
LSN, and from there into the WAL writer, which asks to write and
flush WAL that was never generated. If the page at the boundary
has not been initialized yet, XLogWrite() then PANICs with "xlog
write request ... is past end of log". Otherwise it reports write
and flush positions for WAL that was never written, and in an
assert-enabled build fails an assertion there.

Adjust the end position to the start of the segment (before the
page header) using XLogBytePosToEndRecPtr(), the way it is done
for any other record. The bad position then no longer goes into
the asynchronous commit LSN or the WAL writer.

This also makes pg_switch_wal() return what its documentation says:
the ending location plus 1 within the just-completed WAL file.
---
 src/backend/access/transam/xlog.c | 19 ++++++-------------
 1 file changed, 6 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8636ad9ab2c..601f6599c2c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -1077,21 +1077,14 @@ XLogInsertRecord(XLogRecData *rdata,
 		/*
 		 * Even though we reserved the rest of the segment for us, which is
 		 * reflected in EndPos, we return a pointer to just the end of the
-		 * xlog-switch record.
+		 * xlog-switch record. When the record falls right on the segment
+		 * boundary, EndPos is adjusted to the start of the segment (before
+		 * the page header), so that it doesn't go past the end of reserved
+		 * WAL.
 		 */
 		if (inserted)
-		{
-			EndPos = StartPos + SizeOfXLogRecord;
-			if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ)
-			{
-				uint64		offset = XLogSegmentOffset(EndPos, wal_segment_size);
-
-				if (offset == EndPos % XLOG_BLCKSZ)
-					EndPos += SizeOfXLogLongPHD;
-				else
-					EndPos += SizeOfXLogShortPHD;
-			}
-		}
+			EndPos = XLogBytePosToEndRecPtr(XLogRecPtrToBytePos(StartPos) +
+											MAXALIGN(SizeOfXLogRecord));
 	}
 
 	/* Process any flush progress published while making room for the record. */
-- 
2.50.1 (Apple Git-155)

From 820e5bdc8c9e80777dc3d27307f4152dd5f9cc54 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <[email protected]>
Date: Fri, 23 Oct 2026 20:12:35 +0900
Subject: [PATCH v7 2/2] Add a test for the end LSN of an xlog-switch record

An xlog-switch record that starts SizeOfXLogRecord bytes before a
segment boundary ends exactly on it, and pg_switch_wal() must report
the boundary.  Without the fix, it reports the boundary plus
SizeOfXLogLongPHD.

The test goes into 039_end_of_wal.pl, whose node is set up so that
nothing else writes WAL.  It pads the segment with logical messages,
measuring the record overhead and page header size at run time
instead of hard-coding them.
---
 src/test/recovery/t/039_end_of_wal.pl | 69 +++++++++++++++++++++++++++
 1 file changed, 69 insertions(+)

diff --git a/src/test/recovery/t/039_end_of_wal.pl b/src/test/recovery/t/039_end_of_wal.pl
index 5fe8835e094..fa4ca73fd73 100644
--- a/src/test/recovery/t/039_end_of_wal.pl
+++ b/src/test/recovery/t/039_end_of_wal.pl
@@ -376,4 +376,73 @@ ok( $node->log_contains(
 		"invalid contrecord length 123456 .* at .*", $log_size),
 	"xlp_rem_len bad (split record header)");
 
+
+###########################################################################
+note "xlog-switch record ending on a segment boundary";
+###########################################################################
+
+# A switch record that starts SizeOfXLogRecord bytes before the end of a
+# segment ends exactly on the boundary, and pg_switch_wal() has to return
+# the boundary, not a position past the next page header.
+
+# Emit a non-transactional logical message (no XID, no commit record)
+# with a payload of $size bytes, returning its end LSN in bytes.
+sub emit_message
+{
+	my ($node, $size) = @_;
+	return int(
+		$node->safe_psql(
+			'postgres',
+			"SELECT pg_logical_emit_message(false, '', repeat('a', $size)) - '0/0'"
+		));
+}
+
+my $SizeOfXLogRecord = 24;
+
+# Start on a fresh segment.  Nothing else writes WAL on this node, so the
+# positions below are exact.
+$node->safe_psql('postgres', "SELECT pg_switch_wal()");
+my $lsn = int(
+	$node->safe_psql(
+		'postgres', "SELECT pg_current_wal_insert_lsn() - '0/0'"));
+my $seg_end = ($lsn / $WAL_SEGMENT_SIZE + 1) * $WAL_SEGMENT_SIZE;
+
+# Measure the space a message takes beyond its payload, padding included.
+# 256 is a multiple of MAXALIGN, over 255 so the record uses the long
+# main-data header like the fill, and small enough to stay on the first page.
+my $probe_size = 256;
+my $end = emit_message($node, $probe_size);
+my $overhead = $end - $lsn - $probe_size;
+$lsn = $end;
+
+# Measure the page header size with a message crossing one page boundary.
+# The first page of a segment has a longer header, but nothing below
+# crosses a segment boundary.
+my $size = start_of_next_page($lsn) - $lsn;
+$end = emit_message($node, $size);
+my $SizeOfXLogShortPHD = $end - $lsn - $size - $overhead;
+$lsn = $end;
+
+# Fill the segment in steps, so that no record gets too big for any
+# segment size, until the rest fits in one message.
+my $step = $WAL_SEGMENT_SIZE / 16;
+$lsn = emit_message($node, $step) while $seg_end - $lsn > 2 * $step;
+
+# Size the last message to end SizeOfXLogRecord bytes before the
+# boundary, less the page headers it crosses.  A record ending exactly on
+# a page boundary reports the boundary while the next one starts after
+# the page header, hence the - 1.
+my $headers =
+  ($seg_end - start_of_next_page($lsn - 1)) / $WAL_BLOCK_SIZE *
+  $SizeOfXLogShortPHD;
+$lsn = emit_message($node,
+	$seg_end - $SizeOfXLogRecord - $lsn - $headers - $overhead);
+is($lsn, $seg_end - $SizeOfXLogRecord,
+	"message ends SizeOfXLogRecord bytes before the segment end");
+
+# The switch record is only a header, so it ends on the boundary.
+is( $node->safe_psql('postgres', "SELECT pg_switch_wal() - '0/0'"),
+	$seg_end,
+	"pg_switch_wal() returns the segment boundary");
+
 done_testing();
-- 
2.50.1 (Apple Git-155)

Reply via email to