From 934cefe8e61d5d427105ab2f0f65579ada219eaa Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Mon, 20 Apr 2026 21:27:29 +0300 Subject: [PATCH v5 6/7] Improve WAIT FOR LSN test coverage 1. Check the values of writtenUpto/flushedUpto initialized by RequestXLogStreaming(). Check WAIT FOR LSN succeeds in both standby_write / standby_flush modes thanks to replay-position floor in GetCurrentLSNForWaitType() 2. Off-by-one boundary checks for the wait predicate target <= currentLSN. With replay paused and the walreceiver stopped, verify that targets at current and current - 1 succeed immediately, that current + 1 times out, and that a waiter at current + 1 wakes once replay actually advances past it. 3. Timeline switch on a cascade standby. A waiter on a cascade standby must survive its upstream's promotion: the cascade walreceiver reconnects on the new timeline and the wait completes when WAL on the new timeline reaches the target. Reported-by: Andres Freund Discussion: https://postgr.es/m/1957514.1775526774%40sss.pgh.pa.us Author: Alexander Korotkov --- src/test/recovery/t/049_wait_for_lsn.pl | 226 ++++++++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl index d2610cf0856..c386425a7fd 100644 --- a/src/test/recovery/t/049_wait_for_lsn.pl +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -824,4 +824,230 @@ ok(1, $arc_standby->stop; $arc_primary->stop; +# 10. Fresh-shmem walreceiver startup (29e7dbf5e4d). +# RequestXLogStreaming() initializes writtenUpto/flushedUpto to the +# segment-aligned receiveStart only when receiveStart was invalid. +# Restart the standby with the primary stopped, so the walreceiver cannot +# connect and advance these values past the initial one before we observe it. + +my $rcv_primary = PostgreSQL::Test::Cluster->new('rcv_primary'); +$rcv_primary->init(allows_streaming => 1); +# No background WAL during our probes. +$rcv_primary->append_conf('postgresql.conf', 'autovacuum = off'); +$rcv_primary->start; +$rcv_primary->safe_psql('postgres', + "CREATE TABLE rcv_test AS SELECT generate_series(1,10) AS a"); + +my $rcv_backup = 'rcv_backup'; +$rcv_primary->backup($rcv_backup); + +my $rcv_standby = PostgreSQL::Test::Cluster->new('rcv_standby'); +$rcv_standby->init_from_backup($rcv_primary, $rcv_backup, has_streaming => 1); +$rcv_standby->start; + +# Switch WAL segments mid-stream so the replay ends mid-segment after the +# upcoming standby restart. That guarantees the initial value < +# final replay LSN. +$rcv_primary->safe_psql('postgres', + "INSERT INTO rcv_test VALUES (generate_series(11, 100))"); +$rcv_primary->safe_psql('postgres', "SELECT pg_switch_wal()"); +$rcv_primary->safe_psql('postgres', + "INSERT INTO rcv_test VALUES (generate_series(101, 110))"); +$rcv_primary->wait_for_catchup($rcv_standby); + +# Restart the standby with the primary down: WalRcvData is initialized, but +# the walreceiver cannot connect and update writtenUpto/flushedUpto. So, +# the initial flushedUpto stays observable via pg_last_wal_receive_lsn()). +$rcv_standby->stop; +$rcv_primary->stop; +$rcv_standby->start; + +$rcv_standby->poll_query_until('postgres', + "SELECT pg_last_wal_receive_lsn() <> '0/0'::pg_lsn;") + or die "walreceiver initial value did not become visible"; + +# Freeze the replay so the (received, replay] window stays observable. +$rcv_standby->safe_psql('postgres', "SELECT pg_wal_replay_pause()"); + +my $rcv_written = + $rcv_standby->safe_psql('postgres', "SELECT pg_last_wal_receive_lsn()"); +my $rcv_replay = + $rcv_standby->safe_psql('postgres', "SELECT pg_last_wal_replay_lsn()"); +my $rcv_gap = $rcv_standby->safe_psql('postgres', + "SELECT pg_wal_lsn_diff('$rcv_replay'::pg_lsn, '$rcv_written'::pg_lsn) > 0" +); +ok($rcv_gap eq 't', "replay sits ahead of initial writtenUpto"); + +# WAIT FOR an $rcv_replay LSN succeeds in standby_write / standby_flush +# modes thanks to GetCurrentLSNForWaitType() taking replay LSN as the floor. +foreach my $rcv_mode ('standby_write', 'standby_flush') +{ + $output = $rcv_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${rcv_replay}' + WITH (MODE '$rcv_mode', timeout '5s', no_throw);]); + ok($output eq "success", + "$rcv_mode succeeds for already-replayed LSN after standby restart"); +} + +# Restore primary and resume replay so section 11 can reuse the +# clusters. +$rcv_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume()"); +$rcv_primary->start; +$rcv_primary->wait_for_catchup($rcv_standby); + +# 11. Off-by-one boundary checks for the wait predicate target <= +# currentLSN. Stop the walreceiver before pausing replay (stopping +# after pause can hang -- see section 7d) so both replay and +# walreceiver positions are frozen. +stop_walreceiver($rcv_standby); +$rcv_standby->safe_psql('postgres', "SELECT pg_wal_replay_pause()"); + +my $boundary_lsn = + $rcv_standby->safe_psql('postgres', "SELECT pg_last_wal_replay_lsn()"); +my $boundary_minus = $rcv_standby->safe_psql('postgres', + "SELECT ('$boundary_lsn'::pg_lsn - 1)::text"); +my $boundary_plus = $rcv_standby->safe_psql('postgres', + "SELECT ('$boundary_lsn'::pg_lsn + 1)::text"); + +# 11a. target == current LSN succeeds immediately (predicate is <=). +foreach my $b_mode ('standby_replay', 'standby_write', 'standby_flush') +{ + $output = $rcv_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${boundary_lsn}' + WITH (MODE '$b_mode', timeout '5s', no_throw);]); + ok($output eq "success", "$b_mode: target == current succeeds"); +} + +# 11b. target == current - 1 succeeds immediately. +foreach my $b_mode ('standby_replay', 'standby_write', 'standby_flush') +{ + $output = $rcv_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${boundary_minus}' + WITH (MODE '$b_mode', timeout '5s', no_throw);]); + ok($output eq "success", "$b_mode: target == current - 1 succeeds"); +} + +# 11c. target == current + 1 must time out (no early success). +foreach my $b_mode ('standby_replay', 'standby_write', 'standby_flush') +{ + $output = $rcv_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${boundary_plus}' + WITH (MODE '$b_mode', timeout '500ms', no_throw);]); + ok($output eq "timeout", "$b_mode: target == current + 1 times out"); +} + +# 11d. A sleeping waiter at current + 1 wakes once replay advances +# past it. Resume replay first (safe: walreceiver is stopped so no +# new WAL arrives yet), start the waiter, then restart the +# walreceiver to deliver the new WAL. +$rcv_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume()"); + +$rcv_primary->safe_psql('postgres', + "INSERT INTO rcv_test VALUES (generate_series(200, 210))"); + +my $boundary_log_offset = -s $rcv_standby->logfile; +my $boundary_session = $rcv_standby->background_psql('postgres'); +$boundary_session->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '${boundary_plus}' + WITH (MODE 'standby_replay', timeout '30s', no_throw); + DO \$\$ BEGIN RAISE LOG 'rcv_boundary_done'; END \$\$; +]); + +$rcv_standby->poll_query_until('postgres', + "SELECT count(*) > 0 FROM pg_stat_activity WHERE wait_event = 'WaitForWalReplay'" +) or die "Boundary waiter did not sleep"; + +resume_walreceiver($rcv_standby); +$rcv_standby->wait_for_log(qr/rcv_boundary_done/, $boundary_log_offset); +$boundary_session->quit; + +ok(1, "standby_replay: waiter at current + 1 wakes when replay advances"); + +$rcv_standby->stop; +$rcv_primary->stop; + +# 12. Timeline switch on a cascade standby. A WAIT FOR LSN waiter on +# a cascade standby must survive its upstream's promotion: the +# cascade walreceiver reconnects on the new timeline and replay +# continues across the boundary. + +my $tl_primary = PostgreSQL::Test::Cluster->new('tl_primary'); +$tl_primary->init(allows_streaming => 1); +$tl_primary->append_conf('postgresql.conf', 'autovacuum = off'); +$tl_primary->start; +$tl_primary->safe_psql('postgres', + "CREATE TABLE tl_test AS SELECT generate_series(1, 10) AS a"); + +my $tl_backup = 'tl_backup'; +$tl_primary->backup($tl_backup); + +my $tl_standby1 = PostgreSQL::Test::Cluster->new('tl_standby1'); +$tl_standby1->init_from_backup($tl_primary, $tl_backup, has_streaming => 1); +$tl_standby1->start; + +# standby2 cascades from standby1. +my $tl_backup2 = 'tl_backup2'; +$tl_standby1->backup($tl_backup2); + +my $tl_standby2 = PostgreSQL::Test::Cluster->new('tl_standby2'); +$tl_standby2->init_from_backup($tl_standby1, $tl_backup2, has_streaming => 1); +$tl_standby2->start; + +$tl_primary->safe_psql('postgres', + "INSERT INTO tl_test VALUES (generate_series(11, 20))"); +$tl_primary->wait_for_catchup($tl_standby1); +$tl_standby1->wait_for_catchup($tl_standby2); + +# Target LSN well past current insert LSN, so reaching it requires +# WAL produced on the new timeline. Pause replay on standby2 to +# guarantee the waiter is asleep when the switch happens. +my $tl_target = $tl_primary->safe_psql('postgres', + "SELECT (pg_current_wal_insert_lsn() + 65536)::text"); + +$tl_standby2->safe_psql('postgres', "SELECT pg_wal_replay_pause()"); + +my $tl_log_offset = -s $tl_standby2->logfile; +my $tl_session = $tl_standby2->background_psql('postgres'); +$tl_session->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '${tl_target}' + WITH (MODE 'standby_replay', timeout '60s', no_throw); + DO \$\$ BEGIN RAISE LOG 'tl_wait_done'; END \$\$; +]); + +$tl_standby2->poll_query_until('postgres', + "SELECT count(*) > 0 FROM pg_stat_activity WHERE wait_event = 'WaitForWalReplay'" +) or die "Cascade waiter did not sleep before promotion"; + +# Promote standby1 to TLI 2; produce enough WAL on the new timeline +# to push past tl_target and force a segment switch. +$tl_standby1->promote; +$tl_standby1->safe_psql('postgres', + "INSERT INTO tl_test VALUES (generate_series(21, 1020))"); +$tl_standby1->safe_psql('postgres', "SELECT pg_switch_wal()"); + +$tl_standby2->safe_psql('postgres', "SELECT pg_wal_replay_resume()"); + +$tl_standby2->poll_query_until('postgres', + "SELECT received_tli > 1 FROM pg_stat_wal_receiver") + or die "tl_standby2 did not follow upstream timeline switch"; + +$tl_standby2->wait_for_log(qr/tl_wait_done/, $tl_log_offset); +$tl_session->quit; + +ok(1, + "WAIT FOR LSN survives upstream promotion and timeline switch on cascade standby" +); + +$tl_standby2->stop; +$tl_standby1->stop; +$tl_primary->stop; + done_testing(); -- 2.39.5 (Apple Git-154)