From fd744df4a49dd5b1b7f14113458a546f562ce486 Mon Sep 17 00:00:00 2001
From: ChangAo Chen <cca5507@qq.com>
Date: Tue, 25 Aug 2026 21:31:23 +0800
Subject: [PATCH v2] Fix temporary WAL receiver slot handling on timeline
 switches.

When wal_receiver_create_temp_slot is enabled, a WAL receiver creates a
temporary replication slot before starting streaming.  If it reaches the
end of an old timeline and is instructed to continue on a newer one, it
reuses the same connection but enters the streaming setup again.  Since
is_temp_slot remains true, it attempts to create the same temporary slot
again and fails because the slot already exists.

Track whether the temporary slot has already been created for the lifetime
of the WAL receiver and skip subsequent creation attempts.  Continue to
copy the locally retained slot name to shared memory on each streaming
attempt, since RequestXLogStreaming() clears it when primary_slot_name is
not configured.  This also keeps pg_stat_wal_receiver.slot_name populated
after a timeline switch.

Add coverage for both behaviors to the timeline-switch recovery test.
---
 src/backend/replication/walreceiver.c      | 22 +++++++----
 src/test/recovery/t/004_timeline_switch.pl | 43 ++++++++++++++++++++++
 2 files changed, 58 insertions(+), 7 deletions(-)

diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 61dc6a5588b..26271bacd9d 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -158,6 +158,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len)
 	char	   *tmp_conninfo;
 	char		slotname[NAMEDATALEN];
 	bool		is_temp_slot;
+	bool		temp_slot_created = false;
 	XLogRecPtr	startpoint;
 	TimeLineID	startpointTLI;
 	TimeLineID	primaryTLI;
@@ -431,17 +432,24 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len)
 		WalRcvFetchTimeLineHistoryFiles(startpointTLI, primaryTLI);
 
 		/*
-		 * Create temporary replication slot if requested, and update slot
-		 * name in shared memory.  (Note the slot name cannot already be set
-		 * in this case.)
+		 * Create a temporary replication slot if requested.  This only needs
+		 * to be done for the first stream because the slot remains available
+		 * while this connection is reused for subsequent timelines.  Update
+		 * the slot name in shared memory each time because
+		 * RequestXLogStreaming() clears it when restarting on a new timeline.
 		 */
 		if (is_temp_slot)
 		{
-			snprintf(slotname, sizeof(slotname),
-					 "pg_walreceiver_%lld",
-					 (long long int) walrcv_get_backend_pid(wrconn));
+			if (!temp_slot_created)
+			{
+				snprintf(slotname, sizeof(slotname),
+						 "pg_walreceiver_%lld",
+						 (long long int) walrcv_get_backend_pid(wrconn));
+
+				walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
-			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
+				temp_slot_created = true;
+			}
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/test/recovery/t/004_timeline_switch.pl b/src/test/recovery/t/004_timeline_switch.pl
index f9955b2b4eb..2301aa9e5d0 100644
--- a/src/test/recovery/t/004_timeline_switch.pl
+++ b/src/test/recovery/t/004_timeline_switch.pl
@@ -145,4 +145,47 @@ my $result_2 =
   $node_standby_3->safe_psql('postgres', "SELECT count(*) FROM tab_int");
 is($result_2, qq(1), 'check content of standby 3');
 
+# Ensure that a WAL receiver creates a temporary replication slot only once
+# when following an upstream across a timeline switch.
+
+# Initialize primary node
+my $node_primary_3 = PostgreSQL::Test::Cluster->new('primary_3');
+$node_primary_3->init(allows_streaming => 1);
+$node_primary_3->start;
+
+# Take backup
+$node_primary_3->backup($backup_name);
+
+# Create standby node
+my $node_standby_4 = PostgreSQL::Test::Cluster->new('standby_4');
+$node_standby_4->init_from_backup($node_primary_3, $backup_name,
+	has_streaming => 1);
+$node_standby_4->append_conf(
+	'postgresql.conf', "wal_receiver_create_temp_slot = on");
+
+# Restart primary node in standby mode and promote it, switching it
+# to a new timeline.
+$node_primary_3->set_standby_mode;
+$node_primary_3->restart;
+$node_primary_3->promote;
+
+# Start standby node, create some content on primary and check its presence
+# in standby, to ensure that the timeline switch has been done.
+$node_standby_4->start;
+$node_primary_3->safe_psql('postgres',
+	"CREATE TABLE tab_int AS SELECT 1 AS a");
+$node_primary_3->wait_for_catchup($node_standby_4);
+
+ok( !$node_standby_4->log_contains(
+		'could not create replication slot "pg_walreceiver_[0-9]+".*already exists'
+	),
+	'temporary replication slot is not recreated across timeline jumps');
+
+my $temp_slot_name = $node_standby_4->safe_psql('postgres',
+	"SELECT slot_name FROM pg_stat_wal_receiver");
+like(
+	$temp_slot_name,
+	qr/^pg_walreceiver_[0-9]+$/,
+	'pg_stat_wal_receiver.slot_name remains set across timeline jumps');
+
 done_testing();
-- 
2.53.0

