Hi,
A physical standby can get stuck forever if local pg_wal contains a
corrupt record that walreceiver has already reported as flushed.
What happens:
1. walreceiver streams past the bad record and leaves flushedUpto
ahead of it.
2. Startup reads the corrupt bytes, fails ValidXLogRecordHeader,
and treats streaming as a failed source.
3. It shuts walreceiver down and calls RequestXLogStreaming() from
the start of that record (rounded down to the segment boundary).
4. RequestXLogStreaming() only resets flushedUpto on first start or
a timeline change, so the old high-water mark remains.
5. WaitForWALToBecomeAvailable() sees RecPtr < flushedUpto, decides
the WAL is already on disk, and rereads the same bytes.
6. Startup kills walreceiver again ("terminating walreceiver process
due to administrator command") before START_REPLICATION.
The result is a tight loop: invalid record, launch walreceiver, reread
local WAL, kill walreceiver. Replay never advances.
This still happens on current master. I reproduced it with the TAP
test in 0001: without the C change, 053_stream_repair.pl times out
waiting for replay to pass the injected record; with the change it
passes.
The fix resets flushedUpto (and latestChunkStart) when the requested
start is behind the previous flush pointer. That is the same
RequestXLogStreaming() site that already rewinds on first start and on
timeline change. recptr has already been rounded down to a segment
boundary. After lastSourceFailed the current WAL file is closed, so
startup re-reads the record from its start once the replacement bytes
have been flushed.
This is intentionally not "always rewind flushedUpto". Cai Mengjuan
proposed that in 2021, and Kyotaro Horiguchi pointed out that a
blanket rewind can mix the already-read prefix of a spanning record
with later replacement data [1]. We only rewind when streaming is
explicitly restarted from an earlier LSN.
A related patch was CF 5199 (pixian shi, withdrawn 2026-09-07) [2][3].
That compared recptr against receiveStart. receiveStart is the start
of the current walreceiver session and is not updated as flush
advances, so restarting from the same segment does not rewind even
when flushedUpto is already in a later segment. That is exactly the
hang this TAP test covers. Comparing against flushedUpto also covers
the switchover PANIC from CF 5199, where startup tried to open a WAL
file that existed only according to a stale flush pointer.
Side effect: pg_last_wal_receive_lsn() can move backward across such a
restart. I think that is preferable to reporting WAL that recovery
has just rejected.
I have not changed the XLogData handler to reject a stream that skips
forward inside a segment. That would be a separate invariant.
Tested with:
make -C src/test/recovery check PROVE_TESTS='t/053_stream_repair.pl'
[1]
https://postgr.es/m/20210329.105441.1978082841561262877.horikyota.ntt@gmail.com
[2] https://commitfest.postgresql.org/patch/5199/
[3]
https://postgr.es/m/CAAccyYKrRojjO-weeXFs1EqLFHWSBfjBzObwVDv4u-ZxmU=7Rg@mail.gmail.com
Konstantin Knizhnik (1):
Rewind walreceiver flushedUpto when restarting from an earlier LSN.
src/backend/replication/walreceiverfuncs.c | 15 ++-
src/include/replication/walreceiver.h | 3 +-
src/test/recovery/meson.build | 1 +
src/test/recovery/t/053_stream_repair.pl | 109 +++++++++++++++++++++
4 files changed, 124 insertions(+), 4 deletions(-)
create mode 100644 src/test/recovery/t/053_stream_repair.pl