Test only: reproduce the standby-confirmation lost wakeup. diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -4141,7 +4141,16 @@ * the receipt of the LSN. */ if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION) + { + /* Widen the window before joining the waitlist. */ + if (access("walsnd_race_standby", F_OK) == 0) + { + elog(LOG, "walsnd_race: window open"); + pg_usleep(3000000L); + elog(LOG, "walsnd_race: window closed"); + } ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv); + } else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL) { ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv); diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -69,6 +69,7 @@ 't/058_shutdown_crash_restart.pl', 't/059_remote_apply_status_interval.pl', 't/060_multixact_crashed_updater.pl', + 't/061_walsender_standby_wakeup.pl', ], }, } diff --git a/src/test/recovery/t/061_walsender_standby_wakeup.pl b/src/test/recovery/t/061_walsender_standby_wakeup.pl new file mode 100644 --- /dev/null +++ b/src/test/recovery/t/061_walsender_standby_wakeup.pl @@ -0,0 +1,111 @@ + +# Test only: a logical walsender loses the standby-confirmation wakeup. +# +# With the matching walsender.c delay, the walsender checks the standby slot, +# then sleeps 3 s before joining wal_confirm_rcv_cv. The standby confirms in +# that gap, so the broadcast is lost and the walsender sleeps until its 10 s +# timeout. The assertions pass when the wakeup is lost. An unrelated wakeup +# can mask the race. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 'logical'); +$primary->append_conf( + 'postgresql.conf', qq( +autovacuum = off +wal_sender_timeout = 0 +)); +$primary->start; + +$primary->safe_psql( + 'postgres', qq( + CREATE TABLE tab_int (a int); + SELECT pg_create_physical_replication_slot('sb1_slot'); + SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true); +)); +$primary->append_conf('postgresql.conf', + "synchronized_standby_slots = 'sb1_slot'"); +$primary->reload; + +my $backup_name = 'backup'; +$primary->backup($backup_name); +my $standby1 = PostgreSQL::Test::Cluster->new('standby1'); +$standby1->init_from_backup($primary, $backup_name, has_streaming => 1); +$standby1->append_conf('postgresql.conf', "primary_slot_name = 'sb1_slot'"); +$standby1->start; +$primary->wait_for_replay_catchup($standby1); + +# Stream the failover slot. No status messages, so the client never wakes the +# walsender. +my ($stdout, $stderr); +my $recv = IPC::Run::start( + [ + 'pg_recvlogical', + '--dbname' => $primary->connstr('postgres') . ' application_name=test_slot', + '--slot' => 'test_slot', + '--start', + '--file' => $primary->basedir . '/test_slot.out', + '--status-interval' => '0', + '--fsync-interval' => '0', + ], + '>' => \$stdout, + '2>' => \$stderr); + +$primary->safe_psql('postgres', "INSERT INTO tab_int VALUES (1);"); +$primary->wait_for_catchup('test_slot', 'sent', $primary->lsn('insert')); + +# Enable the delay and pause the walreceiver, so the next commit waits for +# the standby. +my $marker = $primary->data_dir . '/walsnd_race_standby'; +append_to_file($marker, ''); +my $receiverpid = + $standby1->safe_psql('postgres', "SELECT pid FROM pg_stat_wal_receiver"); +like($receiverpid, qr/^[0-9]+$/, "have walreceiver pid $receiverpid"); +kill 'STOP', $receiverpid; + +my $offset = -s $primary->logfile; +my $before = $primary->lsn('insert'); +$primary->safe_psql('postgres', "INSERT INTO tab_int VALUES (2);"); +$offset = $primary->wait_for_log(qr/walsnd_race: window open/, $offset); +unlink $marker; + +# Resume the walreceiver. The standby confirms while the walsender is not yet +# on the waitlist. +kill 'CONT', $receiverpid; +$primary->wait_for_log(qr/walsnd_race: window closed/, $offset); +sleep(1); + +# The standby has confirmed, yet the walsender hasn't sent the commit and is +# still waiting for confirmation. +is( $primary->safe_psql( + 'postgres', qq( + SELECT (SELECT restart_lsn FROM pg_replication_slots + WHERE slot_name = 'sb1_slot') > '$before', + r.sent_lsn <= '$before', a.wait_event + FROM pg_stat_replication r JOIN pg_stat_activity a USING (pid) + WHERE r.application_name = 'test_slot')), + 't|t|WaitForStandbyConfirmation', + 'walsender waits although the standby has confirmed'); + +# The next confirmation wakes it. +$primary->safe_psql('postgres', "INSERT INTO tab_int VALUES (3);"); +$primary->wait_for_catchup('test_slot', 'sent', $primary->lsn('insert')); +pass('walsender resumes after the next confirmation'); + +$recv->signal('TERM'); +$recv->finish(); + +done_testing();