From d22b4cbe80f510d6ad8f3dd95666be5eef39f294 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Sat, 10 Oct 2026 18:50:00 +0000 Subject: [PATCH v10 2/4] Use WALReadFromBuffers() for logical walsenders. Commit 91f2cae7a4 introduced WALReadFromBuffers() but used it only for physical walsenders. This commit lets logical walsenders also read WAL from the WAL buffers instead of always going to a file. This reduces read IO and syscalls, especially when the logical replication consumer keeps up with WAL generation. It helps even more with WAL direct IO enabled, where a file read is always a physical disk read without the OS page cache. Author: Bharath Rupireddy Reviewed-by: Michael Paquier Reviewed-by: Kirill Reshke Discussion: https://postgr.es/m/CALj2ACVfF2Uj9NoFy-5m98HNtjHpuD17EDE9twVeJng-jTAe7A%40mail.gmail.com --- src/backend/replication/walsender.c | 34 +++++++++++++++++----- src/test/subscription/t/001_rep_changes.pl | 9 +++--- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 10bc3e0cae4..2991dd6eb6f 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1098,6 +1098,7 @@ logical_read_xlog_page(XLogReaderState *state, XLogRecPtr targetPagePtr, int req WALReadError errinfo; XLogSegNo segno; TimeLineID currTLI; + Size rbytes; /* * Make sure we have enough WAL available before retrieving the current @@ -1157,16 +1158,33 @@ logical_read_xlog_page(XLogReaderState *state, XLogRecPtr targetPagePtr, int req else count = flushptr - targetPagePtr; /* part of the page available */ - /* now actually read the data, we know it's there */ - if (!WALRead(state, - cur_page, - targetPagePtr, - count, - currTLI, /* Pass the current TLI because only + /* attempt to read WAL from WAL buffers first */ + rbytes = WALReadFromBuffers(cur_page, targetPagePtr, count, currTLI); + + /* now read the remaining WAL from WAL file */ + if (rbytes < count) + { + if (!WALRead(state, + cur_page + rbytes, + targetPagePtr + rbytes, + count - rbytes, + currTLI, /* Pass the current TLI because only * WalSndSegmentOpen controls whether new TLI * is needed. */ - &errinfo)) - WALReadRaiseError(&errinfo); + &errinfo)) + WALReadRaiseError(&errinfo); + } + else if (state->seg.ws_file >= 0 && + !XLByteInSeg(targetPagePtr, state->seg.ws_segno, + state->segcxt.ws_segsize)) + { + /* + * Close the segment when a read that comes entirely from the WAL + * buffers is not in the open segment, so that the next file read + * reopens the correct one. + */ + state->routine.segment_close(state); + } /* * After reading into the buffer, check that what we read was valid. We do diff --git a/src/test/subscription/t/001_rep_changes.pl b/src/test/subscription/t/001_rep_changes.pl index 7d41715ed81..5eb74615bca 100644 --- a/src/test/subscription/t/001_rep_changes.pl +++ b/src/test/subscription/t/001_rep_changes.pl @@ -187,12 +187,13 @@ $result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_no_col"); is($result, qq(2), 'check replicated changes for table having no columns'); -# Wait for the logical WAL sender to update its IO statistics. This is -# done before the next restart, which would force a flush of its stats, and -# far enough from the reset done above to not impact the run time. +# Wait for the logical WAL sender to update its IO statistics (WAL reads from +# buffers and file). This is done before the next restart, which would force a +# flush of its stats, and far enough from the reset done above to not impact +# the run time. $node_publisher->poll_query_until( 'postgres', - qq[SELECT sum(reads) > 0 + qq[SELECT sum(reads) + sum(hits) > 0 FROM pg_catalog.pg_stat_io WHERE backend_type = 'walsender' AND object = 'wal'] -- 2.47.3