From 2ebe6c2ee2336fbdf18261681b33c9ed2d8d25fd Mon Sep 17 00:00:00 2001 From: alterego655 <824662526@qq.com> Date: Sun, 18 Oct 2026 21:04:46 +0800 Subject: [PATCH v12 2/2] Test logical decoding LSN waits across promotion Exercise SQL slot creation waiting for WAL replay across both local promotion and promotion of an upstream standby. Check that decoding continues on the new timeline while a cascading standby remains in recovery, and that local promotion releases the replay wait. Also exercise promotion while pg_sync_replication_slots() is waiting for replay. Pause at a recovery target so further replay cannot satisfy the wait, forcing slot synchronization to handle the shutdown interrupt. Verify that the waiting backend holds a temporary synced slot and that error cleanup removes it before the session disconnects. --- .../t/035_standby_logical_decoding.pl | 72 +++++++++++++++++- .../t/040_standby_failover_slots_sync.pl | 74 +++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl index 88893f71350..562ed7d2ca8 100644 --- a/src/test/recovery/t/035_standby_logical_decoding.pl +++ b/src/test/recovery/t/035_standby_logical_decoding.pl @@ -998,9 +998,50 @@ $node_primary->wait_for_replay_catchup($node_standby); $node_standby->wait_for_replay_catchup($node_cascading_standby, $node_primary); -# promote +# Stop WAL production and replay through the shutdown checkpoint, so slot +# creation cannot finish using old-timeline WAL. +$node_primary->stop; +my $old_end = $node_standby->lsn('receive'); +$node_standby->poll_query_until('postgres', + "SELECT pg_last_wal_replay_lsn() >= '$old_end'::pg_lsn") + or die "standby did not replay the primary's final WAL"; +$node_standby->wait_for_catchup($node_cascading_standby, 'replay', $old_end); + +# Exercise read_local_xlog_page() by starting SQL slot creation before promotion. +my $upstream_decode_session = + $node_cascading_standby->background_psql('testdb'); +$upstream_decode_session->set_query_timer_restart; +my $upstream_decode_pid = + $upstream_decode_session->query_safe('SELECT pg_backend_pid()'); +$upstream_decode_session->query_until( + qr/upstream_slot_creation_started/, q[ + \echo upstream_slot_creation_started + SELECT lsn FROM pg_create_logical_replication_slot('upstream_promotion_wait_slot', 'test_decoding'); +]); +$node_cascading_standby->poll_query_until( + 'testdb', qq[ + SELECT wait_event = 'WaitForWalReplay' + FROM pg_stat_activity WHERE pid = $upstream_decode_pid; +]) or die "slot creation did not wait for WAL replay before upstream promotion"; + +# Promote the upstream while the decoding node remains in recovery. $node_standby->promote; +# Supply a running-xacts record so slot creation can finish. +$node_standby->safe_psql('testdb', + 'SELECT pg_log_standby_snapshot();'); +my $upstream_slot_lsn = $upstream_decode_session->query_safe(''); +is( $node_cascading_standby->safe_psql('testdb', + "SELECT '$upstream_slot_lsn'::pg_lsn > '$old_end'::pg_lsn"), + 't', + 'SQL slot creation completes beyond the upstream timeline fork' +); +is($node_cascading_standby->safe_psql('testdb', 'SELECT pg_is_in_recovery()'), + 't', 'decoding node remains in recovery after upstream promotion'); +$upstream_decode_session->quit; +$node_cascading_standby->safe_psql('testdb', + "SELECT pg_drop_replication_slot('upstream_promotion_wait_slot');"); + # insert some rows on promoted standby $node_standby->safe_psql('testdb', qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(5,7) s;] @@ -1092,6 +1133,7 @@ $node_standby->wait_for_replay_catchup($node_cascading_standby); # Open a background psql session BEFORE promotion for the SQL decoding # test. my $decode_session = $node_cascading_standby->background_psql('testdb'); +$decode_session->set_query_timer_restart; # Attach injection point to pause startup after WAL segment cleanup # but before RecoveryInProgress() flips to false. @@ -1138,8 +1180,36 @@ my $sql_out = $decode_session->query_safe( is($sql_out, $expected, 'pg_logical_slot_peek_changes works during promotion timeline switch'); +# Start SQL slot creation beyond the final replay position. Promotion must +# release its replay wait so read_local_xlog_page() can use the flush position. +my $decode_pid = $decode_session->query_safe('SELECT pg_backend_pid()'); +$decode_session->query_until( + qr/slot_creation_started/, q[ + \echo slot_creation_started + SELECT slot_name FROM pg_create_logical_replication_slot('promotion_wait_slot', 'test_decoding'); +]); +$node_cascading_standby->poll_query_until( + 'testdb', qq[ + SELECT wait_event = 'WaitForWalReplay' + FROM pg_stat_activity WHERE pid = $decode_pid; +]) or die "slot creation did not wait for WAL replay"; + # Resume promotion. $node_cascading_standby->safe_psql('testdb', "SELECT injection_points_wakeup('promotion-after-wal-segment-cleanup');"); +$node_cascading_standby->poll_query_until('testdb', + 'SELECT NOT pg_is_in_recovery()') + or die "promotion did not finish"; + +# Supply a running-xacts record so slot creation can finish. +$node_cascading_standby->safe_psql('testdb', + 'SELECT pg_log_standby_snapshot();'); +is($decode_session->query_safe(''), + 'promotion_wait_slot', + 'SQL slot creation waiting for WAL completes after promotion'); +$decode_session->quit; +$node_cascading_standby->safe_psql('testdb', + "SELECT pg_drop_replication_slot('promotion_wait_slot');"); + done_testing(); diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl index f8922aaa1a2..a31600ec613 100644 --- a/src/test/recovery/t/040_standby_failover_slots_sync.pl +++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl @@ -1106,4 +1106,78 @@ $standby2->wait_for_log( $h->quit; +# Test that promotion interrupts SQL slot synchronization waiting for WAL. +# Stop at a recovery target so promotion cannot satisfy the wait by replaying +# more WAL before shutting down slot synchronization. +$subscriber1->safe_psql('postgres', + "ALTER SUBSCRIPTION regress_mysub1 DISABLE"); +$primary->poll_query_until('postgres', + "SELECT NOT active FROM pg_replication_slots WHERE slot_name = 'lsub1_slot'") + or die "subscription did not stop"; +$primary->wait_for_replay_catchup($standby2); + +$standby2->append_conf( + 'postgresql.conf', qq( +recovery_target_name = 'slotsync_shutdown' +recovery_target_action = 'pause' +)); +$standby2->restart; +$primary->safe_psql('postgres', + "SELECT pg_create_restore_point('slotsync_shutdown');"); +$standby2->poll_query_until('postgres', + "SELECT pg_get_wal_replay_pause_state() = 'paused'") + or die "standby did not pause at the recovery target"; + +# The new slot requires WAL beyond the final replay position. Receive and +# flush it so synchronization attempts to decode it rather than skipping it. +my $shutdown_slot_lsn = $primary->safe_psql( + 'postgres', + "SELECT lsn FROM pg_create_logical_replication_slot('shutdown_wait_slot', 'test_decoding', false, false, true);" +); +$primary->wait_for_catchup($standby2, 'flush', $shutdown_slot_lsn); + +my $sync_session = + $standby2->background_psql('postgres', on_error_stop => 0); +$sync_session->set_query_timer_restart; +my $sync_pid = $sync_session->query_safe('SELECT pg_backend_pid()'); +$sync_session->query_until( + qr/slot_sync_started/, q[ + \echo slot_sync_started + SELECT pg_sync_replication_slots(); +]); +$standby2->poll_query_until( + 'postgres', qq[ + SELECT wait_event = 'WaitForWalReplay' + FROM pg_stat_activity WHERE pid = $sync_pid; +]) or die "slot synchronization did not wait for WAL replay"; + +# The waiting backend must be the one holding the new temporary synced slot. +is( $standby2->safe_psql( + 'postgres', qq[ + SELECT temporary AND synced AND active_pid = $sync_pid + FROM pg_replication_slots + WHERE slot_name = 'shutdown_wait_slot'; +]), + 't', + 'SQL synchronization holds the temporary synced slot while waiting for WAL' +); + +$log_offset = -s $standby2->logfile; +$standby2->promote; +$standby2->wait_for_log( + qr/replication slot synchronization will stop because promotion is triggered/, + $log_offset); + +# Check cleanup before closing the session, which also drops temporary slots. +is( $standby2->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_replication_slots WHERE slot_name = 'shutdown_wait_slot'" + ), + '0', + 'promotion cleans up the temporary slot of interrupted SQL synchronization' +); +$sync_session->quit; +$primary->safe_psql('postgres', + "SELECT pg_drop_replication_slot('shutdown_wait_slot');"); + done_testing(); -- 2.51.0