diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c index b93e699ba4b..ce28ba623cd 100644 --- a/src/backend/replication/walreceiver.c +++ b/src/backend/replication/walreceiver.c @@ -1196,8 +1196,8 @@ XLogWalRcvClose(XLogRecPtr recptr, TimeLineID tli) * The message is sent if 'force' is set, if enough time has passed since the * last update to reach wal_receiver_status_interval, or if WAL locations have * advanced since the previous status update. If wal_receiver_status_interval - * is disabled and 'force' is false, this function does nothing. Set 'force' to - * send the message unconditionally. + * is disabled and neither 'force' nor 'checkApply' is set, this function does + * nothing. Set 'force' to send the message unconditionally. * * Whether WAL locations are considered "advanced" depends on 'checkApply'. * If 'checkApply' is false, only the write and flush locations are checked. @@ -1222,10 +1222,11 @@ XLogWalRcvSendReply(bool force, bool requestReply, bool checkApply) TimestampTz now; /* - * If the user doesn't want status to be reported to the primary, be sure - * to exit before doing anything at all. + * If status reporting is disabled, skip non-forced replies unless the + * startup process has requested an apply notification. Backends waiting + * with synchronous_commit = remote_apply depend on these notifications. */ - if (!force && wal_receiver_status_interval <= 0) + if (!force && !checkApply && wal_receiver_status_interval <= 0) return; /* Get current timestamp. */ diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index aee97e4da2a..e1cf57ff611 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -67,6 +67,7 @@ tests += { 't/056_standby_snapshot_export.pl', 't/057_snapshot_commit_race.pl', 't/058_shutdown_crash_restart.pl', + 't/059_remote_apply_status_interval.pl', ], }, } diff --git a/src/test/recovery/t/059_remote_apply_status_interval.pl b/src/test/recovery/t/059_remote_apply_status_interval.pl new file mode 100644 index 00000000000..31feb1d68cc --- /dev/null +++ b/src/test/recovery/t/059_remote_apply_status_interval.pl @@ -0,0 +1,68 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Check that a commit with synchronous_commit = remote_apply completes when +# the standby has wal_receiver_status_interval = 0. Periodic status updates +# are disabled then, but the walreceiver must still send the apply reply +# requested by the startup process. +# +# Disable timeout-driven keepalives from the walsender and pings from the +# walreceiver, so that they cannot trigger a status reply that would mask +# a missing apply notification. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use Test::More; + +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); + +# Configure synchronous replication at startup, so that there is no window +# where the standby is reported as synchronous before the backends know that +# they have to wait. Use synchronous_commit = local so that the setup does not +# wait for the standby, which is not available yet. +$primary->append_conf( + 'postgresql.conf', qq( +wal_sender_timeout = 0 +synchronous_standby_names = '*' +synchronous_commit = local +)); +$primary->start; +$primary->safe_psql('postgres', 'CREATE TABLE t (a int)'); +$primary->backup('bkp'); + +my $standby = PostgreSQL::Test::Cluster->new('standby'); +$standby->init_from_backup($primary, 'bkp', has_streaming => 1); +$standby->append_conf( + 'postgresql.conf', qq( +wal_receiver_status_interval = 0 +wal_receiver_timeout = 0 +hot_standby_feedback = off +)); +$standby->start; + +$primary->poll_query_until( + 'postgres', + "SELECT count(*) = 1 FROM pg_stat_replication + WHERE state = 'streaming' AND sync_state = 'sync' + AND flush_lsn IS NOT NULL" +) or die "timed out waiting for standby to become synchronous"; + +# The commit must complete even with periodic status updates disabled. +# If the standby fails to report that the commit has been applied, +# query_safe() fails when the session timeout expires. +my $bg = $primary->background_psql('postgres'); +$bg->query_safe('SET synchronous_commit = remote_apply'); +$bg->query_safe('INSERT INTO t VALUES (1)'); +pass('remote_apply commit completes with wal_receiver_status_interval = 0'); + +is($standby->safe_psql('postgres', 'SELECT count(*) FROM t'), + '1', 'remote_apply commit is visible on standby'); + +$bg->quit; + +$standby->stop; +$primary->stop; + +done_testing();