Dear Hayato,
Thanks for your comments! Updated the patch.

On Jan 15, 2026 at 11:48 AM  Hayato Kuroda
<kuroda(dot)hayato(at)fujitsu(dot)com> wrote:
> I think we can just use stop() because it internally runs `pg_ctl stop`
> and that command waits till the wait is finished by default. I feel it
> is dangerous to determine timeout to 5sec because the test can work on
> very poor environment.

ok_with_timeout was added because it allows to output a more reasonable
log in case of a problem from this thread: "Failed test 'Successful fast
shutdown of server with empty output buffers (timed out after 5 seconds)'"
instead of the usual "pg_ctl stop failed".  But now I noticed that the
standard timeout  is triggered earlier, and when setting a timeout in this
function greater than the standard PGCTLTIMEOUT, only "pg_ctl stop failed"
will be written. Perhaps it is reasonable to remove these functions.

> Also, not sure, how can we ensure the buffer is full here? Also, even
> if we have the way to check, the size may be quite platform depending.
> I think it may be better to test both streaming and logical replication
> instead of testing empty/full output buffer. Thought?

Initially, a second test case was added to show that previous patches
did not fix the problem of hanging in case of full buffers. I agree that
it may depend on the platform, but I can't think of a way to guarantee this,
even though the test case seems useful for checking the new mode.
Test contains only the case of logical replication, since so far I'm not
sure how to reproduce guaranteed flush delay on a physical replica in the
test. Any ideas?

Regards,
Andrey Silitskiy
From a898fe6d070f081fdcff2598baccdfe38ab3198c Mon Sep 17 00:00:00 2001
From: "a.silitskiy" <[email protected]>
Date: Fri, 28 Nov 2025 14:40:10 +0700
Subject: [PATCH v3] Introduce a new GUC 'wal_sender_shutdown_mode'.

Previously, at shutdown, walsender processes were always waiting to send all
pending data and ensure that all data is flushed in remote node. But in some cases
an unexpected wait may be unacceptable. For example, in logical replication,
apply_workers may hang on locks for some time, excluding the possibility of
sender's shutdown.

New guc allows to change shutdown mode of walsenders without changing
default behavior.

The shutdown modes are:

1) 'wait_flush' (the default). In this mode, the walsender will wait for all
WALs to be flushed on the receiver side, before exiting the process.

2) 'immediate'. In this mode, the walsender will exit without confirming the
remote flush. This may break the consistency between sender and receiver.
This mode might be useful for a system that has a high-latency network (to
reduce the amount of time for shutdown), or to allow the shutdown of
publisher even when when the subscriber's apply_worker is waiting for any
locks to be released.

Author: Andrey Silitskiy
Co-authored by: Hayato Kuroda
Discussion: https://postgr.es/m/TYAPR01MB586668E50FC2447AD7F92491F5E89%40TYAPR01MB5866.jpnprd01.prod.outlook.com
---
 doc/src/sgml/config.sgml                      |  33 ++++++
 src/backend/replication/walsender.c           |  46 ++++++++
 src/backend/utils/misc/guc_parameters.dat     |   7 ++
 src/backend/utils/misc/guc_tables.c           |   6 ++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/walsender.h           |   7 ++
 src/test/subscription/meson.build             |   1 +
 .../t/037_walsnd_immediate_shutdown.pl        | 100 ++++++++++++++++++
 8 files changed, 202 insertions(+)
 create mode 100644 src/test/subscription/t/037_walsnd_immediate_shutdown.pl

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..79218965cf7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4722,6 +4722,39 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-wal_sender_shutdown_mode" xreflabel="wal_sender_shutdown_mode">
+      <term><varname>wal_sender_shutdown_mode</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>wal_sender_shutdown_mode</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the mode in which walsender process will terminate
+        after receival of shutdown request. Valid values are <literal>wait_flush</literal>
+        and <literal>immediate</literal>. Default value is <literal>wait_flush</literal>.
+        Can be set for each walsender.
+       </para>
+       <para>
+        In <literal>wait_flush</literal> mode, the walsender will wait for all
+        WALs to be flushed on the receiver side, before exiting the process. May
+        lead to unexpected lag of server shutdown.
+       </para>
+       <para>
+        In <literal>immediate</literal> mode, the walsender will exit without waiting
+        for data replication to the receiver. This may break data consistency between
+        sender and receiver after shutdown, which can be especially important in
+        case of physical replication and switch-over.
+       </para>
+       <para>
+        This mode might be useful for a system that has a high-latency network (to
+        reduce the amount of time for shutdown), or to allow the shutdown of
+        logical replication walsender even when the subscriber's apply_worker
+        is waiting for any locks to be released.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-synchronized-standby-slots" xreflabel="synchronized_standby_slots">
       <term><varname>synchronized_standby_slots</varname> (<type>string</type>)
       <indexterm>
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 1ab09655a70..b1e24c7ca3b 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -130,6 +130,9 @@ int			max_wal_senders = 10;	/* the maximum number of concurrent
 									 * walsenders */
 int			wal_sender_timeout = 60 * 1000; /* maximum time to send one WAL
 											 * data message */
+
+int			wal_sender_shutdown_mode = WALSND_SHUTDOWN_MODE_WAIT_FLUSH;
+
 bool		log_replication_commands = false;
 
 /*
@@ -262,6 +265,7 @@ static void WalSndKill(int code, Datum arg);
 pg_noreturn static void WalSndShutdown(void);
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
+pg_noreturn static void WalSndDoneImmediate(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
 static void IdentifySystem(void);
 static void UploadManifest(void);
@@ -1657,6 +1661,11 @@ ProcessPendingWrites(void)
 		/* Try to flush pending output to the client */
 		if (pq_flush_if_writable() != 0)
 			WalSndShutdown();
+
+		/* If we got shut down request in immediate shutdown mode, exit the process */
+		if ((got_STOPPING || got_SIGUSR2) &&
+			 wal_sender_shutdown_mode == WALSND_SHUTDOWN_MODE_IMMEDIATE)
+			WalSndDoneImmediate();
 	}
 
 	/* reactivate latch so WalSndLoop knows to continue */
@@ -2934,6 +2943,14 @@ WalSndLoop(WalSndSendDataCallback send_data)
 		if (pq_flush_if_writable() != 0)
 			WalSndShutdown();
 
+		/*
+		 * When immediate shutdown of walsender is requested, we do not
+		 * wait for successfull sending of all data.
+		 */
+		if ((got_STOPPING || got_SIGUSR2) &&
+			 wal_sender_shutdown_mode == WALSND_SHUTDOWN_MODE_IMMEDIATE)
+			WalSndDoneImmediate();
+
 		/* If nothing remains to be sent right now ... */
 		if (WalSndCaughtUp && !pq_is_send_pending())
 		{
@@ -3582,6 +3599,35 @@ XLogSendLogical(void)
 	}
 }
 
+/*
+ * Shutdown walsender in immediate mode.
+ *
+ * NB: This should only be called when immediate shutdown of walsender
+ * was requested and shutdown signal has been received from postmaster.
+ */
+static void
+WalSndDoneImmediate()
+{
+	QueryCompletion qc;
+
+	/* Try to inform receiver that XLOG streaming is done */
+	SetQueryCompletion(&qc, CMDTAG_COPY, 0);
+	EndCommand(&qc, DestRemote, false);
+
+	/*
+	 * Note that the output buffer may be full during immediate shutdown of
+	 * walsender. If pq_flush() is called at that time, the walsender process
+	 * will be stuck. Therefore, call pq_flush_if_writable() instead. Successfull
+	 * receival of done message in immediate shutdown mode is not guaranteed.
+	 */
+	pq_flush_if_writable();
+
+	ereport(WARNING,
+			(errmsg("walsender shutting down in immediate mode: replication may be incomplete")));
+	proc_exit(0);
+	abort();
+}
+
 /*
  * Shutdown if the sender is caught up.
  *
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..96433fa6015 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3431,6 +3431,13 @@
   check_hook => 'check_wal_segment_size',
 },
 
+{ name => 'wal_sender_shutdown_mode', type => 'enum', context => 'PGC_USERSET', group => 'REPLICATION_SENDING',
+  short_desc => 'Sets the mode in which walsender will be terminated after shutdown request.',
+  variable => 'wal_sender_shutdown_mode',
+  boot_val => 'WALSND_SHUTDOWN_MODE_WAIT_FLUSH',
+  options => 'wal_sender_shutdown_mode_options',
+},
+
 { name => 'wal_sender_timeout', type => 'int', context => 'PGC_USERSET', group => 'REPLICATION_SENDING',
   short_desc => 'Sets the maximum time to wait for WAL replication.',
   flags => 'GUC_UNIT_MS',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 73ff6ad0a32..f2cbb0c700a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -335,6 +335,12 @@ static const struct config_enum_entry constraint_exclusion_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry wal_sender_shutdown_mode_options[] = {
+	{"wait_flush", WALSND_SHUTDOWN_MODE_WAIT_FLUSH, false},
+	{"immediate", WALSND_SHUTDOWN_MODE_IMMEDIATE, false},
+	{NULL, 0, false}
+};
+
 /*
  * Although only "on", "off", "remote_apply", "remote_write", and "local" are
  * documented, we accept all the likely variants of "on" and "off".
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..d3c9c1d3d54 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -346,6 +346,8 @@
 #wal_sender_timeout = 60s       # in milliseconds; 0 disables
 #track_commit_timestamp = off   # collect timestamp of transaction commit
                                 # (change requires restart)
+#wal_sender_shutdown_mode = wait_flush  # walsender termination mode after
+                                # receival of shutdown request
 
 # - Primary Server -
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index a4df3b8e0ae..3f962ea2792 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -24,6 +24,12 @@ typedef enum
 	CRS_USE_SNAPSHOT,
 } CRSSnapshotAction;
 
+typedef enum
+{
+	WALSND_SHUTDOWN_MODE_WAIT_FLUSH = 0,
+	WALSND_SHUTDOWN_MODE_IMMEDIATE
+} WalSndShutdownMode;
+
 /* global state */
 extern PGDLLIMPORT bool am_walsender;
 extern PGDLLIMPORT bool am_cascading_walsender;
@@ -33,6 +39,7 @@ extern PGDLLIMPORT bool wake_wal_senders;
 /* user-settable parameters */
 extern PGDLLIMPORT int max_wal_senders;
 extern PGDLLIMPORT int wal_sender_timeout;
+extern PGDLLIMPORT int wal_sender_shutdown_mode;
 extern PGDLLIMPORT bool log_replication_commands;
 
 extern void InitWalSender(void);
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index a4c7dbaff59..3b9d27fba0b 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -46,6 +46,7 @@ tests += {
       't/034_temporal.pl',
       't/035_conflicts.pl',
       't/036_sequences.pl',
+      't/037_walsnd_immediate_shutdown.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/037_walsnd_immediate_shutdown.pl b/src/test/subscription/t/037_walsnd_immediate_shutdown.pl
new file mode 100644
index 00000000000..890653cc91d
--- /dev/null
+++ b/src/test/subscription/t/037_walsnd_immediate_shutdown.pl
@@ -0,0 +1,100 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Checks that publisher is able to shut down without
+# waiting for sending of all pending data to subscriber
+# with wal_sender_shutdown_mode = immediate
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+# create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+# set wal_sender_shutdown_mode GUC parameter to immediate
+$publisher->append_conf('postgresql.conf',
+	'wal_sender_timeout = 0
+	 wal_sender_shutdown_mode = immediate');
+$publisher->start();
+
+# create subscriber
+my $subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$subscriber->init();
+$subscriber->start();
+
+# create publication for test table
+$publisher->safe_psql('postgres', q{
+	CREATE TABLE pub_test (id int PRIMARY KEY);
+	CREATE PUBLICATION pub_all FOR TABLE pub_test;
+});
+
+# create matching table on subscriber
+$subscriber->safe_psql('postgres', q{
+	CREATE TABLE pub_test (id int PRIMARY KEY);
+});
+
+# form connection string to publisher
+my $pub_connstr = $publisher->connstr;
+
+# create the subscription on subscriber
+$subscriber->safe_psql('postgres', qq{
+	CREATE SUBSCRIPTION sub_all
+	CONNECTION '$pub_connstr'
+	PUBLICATION pub_all;
+});
+
+# wait for initial sync to finish
+$subscriber->wait_for_subscription_sync($publisher, 'sub_all');
+
+# create background psql session
+my $bpgsql = $subscriber->background_psql('postgres', on_error_stop => 0);
+
+
+# =============================================================================
+# Testcase start: Shutdown of publisher with empty output buffers
+
+# start transaction on subscriber to hold locks
+$bpgsql->query_safe(
+	"BEGIN; INSERT INTO pub_test VALUES (0);"
+);
+
+# run concurrent transaction on publisher and commit
+$publisher->safe_psql('postgres', 'BEGIN; INSERT INTO pub_test VALUES (0); COMMIT;');
+
+# test publisher shutdown
+$publisher->stop('fast');
+pass('successfull fast shutdown of server with empty output buffers');
+
+# Testcase end: Shutdown of publisher with empty output buffers
+# =============================================================================
+
+$bpgsql->query_safe(
+	"ABORT;"
+);
+
+# restart publisher for the next testcase
+$publisher->start();
+
+$publisher->wait_for_catchup('sub_all');
+
+# =============================================================================
+# Testcase start: Shutdown of publisher with full output buffers
+
+# lock table to make apply_worker hang
+$bpgsql->query_safe(
+	"BEGIN; LOCK TABLE pub_test IN EXCLUSIVE MODE;"
+);
+
+# generate big amount of wal records for locked table
+$publisher->safe_psql('postgres', 'BEGIN; INSERT INTO pub_test SELECT i from generate_series(1, 50000) s(i); COMMIT;');
+
+# test publisher shutdown
+$publisher->stop('fast');
+pass('successfull fast shutdown of server with full output buffers');
+
+# Testcase end: Shutdown of publisher with full output buffers
+# =============================================================================
+
+done_testing();
-- 
2.34.1

Reply via email to