From e5d8721c0b1a25b2e1c6a87d76f83fec13868786 Mon Sep 17 00:00:00 2001 From: alterego655 <824662526@qq.com> Date: Fri, 10 Apr 2026 11:09:29 +0800 Subject: [PATCH v4 5/5] Wake standby_write/standby_flush waiters from the WAL replay loop The startup process only woke STANDBY_REPLAY waiters after replaying each WAL record. STANDBY_WRITE and STANDBY_FLUSH waiters depended only on walreceiver write/flush callbacks. As a result, replay progress alone did not wake those waiters, and in pure archive recovery (where no walreceiver exists) they could sleep until timeout. Fix by also calling WaitLSNWakeup() for STANDBY_WRITE and STANDBY_FLUSH after each replay. For the replay-floor semantics used by GetCurrentLSNForWaitType(), replay progress is a valid lower bound for both modes: WAL cannot be replayed unless it has already been written and flushed locally. This works together with the replay-position floor in GetCurrentLSNForWaitType(). The getter ensures that a waiter woken by replay can recheck successfully; the replay-side wakeups ensure that a waiter already asleep is notified when replay reaches its target. Reported-by: Tom Lane Discussion: https://postgr.es/m/1957514.1775526774%40sss.pgh.pa.us Author: Xuneng Zhou --- src/backend/access/transam/xlogrecovery.c | 10 ++- src/test/recovery/t/049_wait_for_lsn.pl | 77 +++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 4f2eaa36990..73b78a83fa7 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -1782,11 +1782,17 @@ PerformWalRecovery(void) ApplyWalRecord(xlogreader, record, &replayTLI); /* - * Wake up processes waiting for standby replay LSN to reach - * current replay position. + * Wake up processes waiting for standby replay, write, or flush + * LSN to reach current replay position. Replay implies that the + * WAL was already written and flushed to disk, so write and flush + * waiters can be woken at the replay position too. */ WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_REPLAY, XLogRecoveryCtl->lastReplayedEndRecPtr); + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_WRITE, + XLogRecoveryCtl->lastReplayedEndRecPtr); + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_FLUSH, + XLogRecoveryCtl->lastReplayedEndRecPtr); /* Exit loop if we reached inclusive recovery target */ if (recoveryStopsAfter(xlogreader)) diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl index 8e8776f3ea4..8f623c475db 100644 --- a/src/test/recovery/t/049_wait_for_lsn.pl +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -668,6 +668,17 @@ $arc_primary->start; $arc_primary->safe_psql('postgres', "CREATE TABLE arc_test AS SELECT generate_series(1,10) AS a"); +# Create a helper function for server-side logging (needed by 9b). +$arc_primary->safe_psql( + 'postgres', qq[ +CREATE FUNCTION arc_log_done(msg text) RETURNS void AS \$\$ + BEGIN + RAISE LOG '%', msg; + END +\$\$ +LANGUAGE plpgsql; +]); + my $arc_backup_name = 'arc_backup'; $arc_primary->backup($arc_backup_name); @@ -722,6 +733,72 @@ $output = $arc_standby->safe_psql( ok($output eq "success", "standby_flush succeeds on archive-only standby (getter fallback)"); +# 9b. Replay waker: standby_write/standby_flush waiters that go to sleep +# (target > replay at entry) are woken when replay catches up. This tests +# that PerformWalRecovery() calls WaitLSNWakeup for STANDBY_WRITE and +# STANDBY_FLUSH, not just STANDBY_REPLAY. +# +# Pause replay, archive more WAL, start background waiters, then resume +# replay and verify the waiters complete. + +$arc_standby->safe_psql('postgres', "SELECT pg_wal_replay_pause()"); + +# Generate more WAL and archive it. +$arc_primary->safe_psql('postgres', + "INSERT INTO arc_test VALUES (generate_series(21, 30))"); +my $arc_target_lsn2 = + $arc_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); + +my $arc_segment2 = $arc_primary->safe_psql('postgres', + "SELECT pg_walfile_name(pg_current_wal_lsn())"); +$arc_primary->safe_psql('postgres', "SELECT pg_switch_wal()"); +$arc_primary->poll_query_until('postgres', + qq{SELECT last_archived_wal >= '$arc_segment2' FROM pg_stat_archiver}, + 't') + or die "Timed out waiting for WAL archiving on arc_primary (round 2)"; + +# Start background waiters. With replay paused, target > replay, so they +# will sleep on WaitLatch. They can only be woken by the replay-loop +# WaitLSNWakeup calls. +my $arc_write_session = $arc_standby->background_psql('postgres'); +$arc_write_session->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '${arc_target_lsn2}' + WITH (MODE 'standby_write', timeout '30s', no_throw); + SELECT arc_log_done('write_arc_done'); +]); + +my $arc_flush_session = $arc_standby->background_psql('postgres'); +$arc_flush_session->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '${arc_target_lsn2}' + WITH (MODE 'standby_flush', timeout '30s', no_throw); + SELECT arc_log_done('flush_arc_done'); +]); + +# Verify both waiters are blocked. +$arc_standby->poll_query_until('postgres', + "SELECT count(*) = 2 FROM pg_stat_activity WHERE wait_event LIKE 'WaitForWal%'" +) or die "Timed out waiting for arc_standby waiters to block"; + +# Resume replay. The startup process should wake the STANDBY_WRITE and +# STANDBY_FLUSH waiters as it replays past arc_target_lsn2. +my $arc_log_offset = -s $arc_standby->logfile; +$arc_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume()"); + +# Wait for both sessions to complete. +$arc_standby->wait_for_log(qr/write_arc_done/, $arc_log_offset); +$arc_standby->wait_for_log(qr/flush_arc_done/, $arc_log_offset); + +$arc_write_session->quit; +$arc_flush_session->quit; + +ok(1, + "standby_write/standby_flush waiters woken by replay on archive-only standby" +); + $arc_standby->stop; $arc_primary->stop; -- 2.51.0