From 52c6be27929da5b169f6daab735e35ae4446e88c Mon Sep 17 00:00:00 2001 From: Vaibhav Dalvi Date: Tue, 6 Oct 2026 11:51:39 +0000 Subject: [PATCH v2 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 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 | 82 +++++++++++++++++++ 3 files changed, 89 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..dfdd70f9e1d --- /dev/null +++ b/src/test/recovery/t/058_remote_apply_status_interval.pl @@ -0,0 +1,82 @@ +# 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); +$primary->append_conf('postgresql.conf', "wal_sender_timeout = 0"); +$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; + +# Make the standby synchronous only now, so that the setup above does not +# wait for it. +$primary->safe_psql('postgres', + "ALTER SYSTEM SET synchronous_standby_names = '*'"); +$primary->reload; +$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"; + +# Pause replay so that the commit cannot be applied yet, and check that the +# backend really enters the SyncRep wait. Otherwise the idle check below +# could pass before the INSERT has even started. +$standby->safe_psql('postgres', 'SELECT pg_wal_replay_pause()'); + +my $bg = $primary->background_psql('postgres', on_error_stop => 0); +my $pid = $bg->query_safe('SELECT pg_backend_pid()'); +$bg->query_safe('SET synchronous_commit = remote_apply'); +$bg->query_until(qr/start/, "\\echo start\nINSERT INTO t VALUES (1);\n"); + +ok( $primary->poll_query_until( + 'postgres', + "SELECT wait_event = 'SyncRep' FROM pg_stat_activity + WHERE pid = $pid"), + 'remote_apply commit waits for standby replay'); + +$standby->safe_psql('postgres', 'SELECT pg_wal_replay_resume()'); + +ok( $primary->poll_query_until( + 'postgres', + "SELECT state = 'idle' FROM pg_stat_activity WHERE pid = $pid"), + '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'); + +# Unblock the session if it is still waiting. The wait above may have used +# up the session timer, so restart it before quitting. +$primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"); +$bg->set_query_timer_restart; +$bg->query('SELECT 1'); +$bg->quit; + +$standby->stop; +$primary->stop; + +done_testing(); -- 2.43.0