From 8c259ed343a5ed9c00392934ada0c0af68d4657a Mon Sep 17 00:00:00 2001 From: Vaibhav Dalvi Date: Wed, 7 Oct 2026 04:07:20 +0000 Subject: [PATCH v3 1/1] walreceiver: send apply reply even when wal_receiver_status_interval = 0 XLogWalRcvSendReply() was returning early whenever wal_receiver_status_interval was 0, even if the startup process had asked for an apply notification. Because of this, a backend on the primary doing commit with synchronous_commit = remote_apply kept waiting until some other message carried the apply position. If walsender keepalives and walreceiver pings were also disabled, it would wait forever. Now we do not take this early exit when checkApply is set, so the apply notification requested by the startup process is always sent. This is same as what the documentation of wal_receiver_status_interval already says. The check against previously reported positions is still there, so no duplicate replies are sent. Author: Vaibhav Dalvi Reviewed-by: Ashutosh Sharma Reviewed-by: Fujii Masao Discussion: https://www.postgresql.org/message-id/flat/CA%2BvB%3DAHKUhc9GCDwoLr%3DSshTBCQG09kp8TEXew50UtD5rYqowg%40mail.gmail.com --- src/backend/replication/walreceiver.c | 10 +-- src/test/recovery/meson.build | 1 + .../t/058_remote_apply_status_interval.pl | 67 +++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/test/recovery/t/058_remote_apply_status_interval.pl diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c index b93e699ba4b..eaccf402ce9 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. @@ -1223,9 +1223,11 @@ XLogWalRcvSendReply(bool force, bool requestReply, bool checkApply) /* * If the user doesn't want status to be reported to the primary, be sure - * to exit before doing anything at all. + * to exit before doing anything at all. Apply notifications requested by + * the startup process are still sent, since backends waiting with + * synchronous_commit = remote_apply depend on them. */ - 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 ebb12dd8766..2e2119e33e9 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -66,6 +66,7 @@ tests += { 't/055_cascade_reconnect.pl', 't/056_standby_snapshot_export.pl', 't/057_snapshot_commit_race.pl', + 't/058_remote_apply_status_interval.pl', ], }, } diff --git a/src/test/recovery/t/058_remote_apply_status_interval.pl b/src/test/recovery/t/058_remote_apply_status_interval.pl new file mode 100644 index 00000000000..16775d6d34b --- /dev/null +++ b/src/test/recovery/t/058_remote_apply_status_interval.pl @@ -0,0 +1,67 @@ +# 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. +# +# Keepalives from the walsender and pings from the walreceiver are disabled +# too, so the apply reply is the only message that can release the waiter. + +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 => 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"; + +# Without the fix, this INSERT is never acknowledged by the standby, and +# 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(); -- 2.43.0