From a66192994b42d09a2a4134b5e655d4fa7c15aa61 Mon Sep 17 00:00:00 2001
From: Jesper St John <jesper@stjohn.se>
Date: Wed, 28 Oct 2026 11:22:27 +0100
Subject: [PATCH v2 2/2] pg_basebackup: Drop a lingering slot if the backup
 fails to start

Register an atexit handler, once the slot has been created, that drops
it if pg_basebackup exits before the background WAL receiver has taken
over the connection.

Add tests to 010_pg_basebackup.pl that use a bogus
--manifest-checksums value to fail BASE_BACKUP after the slot has been
created, and check that only the new slot is dropped.
---
 src/bin/pg_basebackup/pg_basebackup.c        | 23 +++++++++++
 src/bin/pg_basebackup/t/010_pg_basebackup.pl | 42 ++++++++++++++++++++
 2 files changed, 65 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 18e36451290..86144f01aa4 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -317,6 +317,22 @@ kill_bgchild_atexit(void)
 }
 #endif
 
+/*
+ * Drop the lingering slot created by PrepareLogStreamer() if we fail before the
+ * background WAL receiver has taken over its connection.  Only registered
+ * once the slot exists, so a pre-existing slot of the same name should never
+ * be dropped.
+ */
+static void
+drop_lingering_replication_slot_atexit(void)
+{
+	/* Check that we have a good state */
+	if (success || in_log_streamer || bgchild > 0)
+		return;
+
+	(void) DropReplicationSlot(bgconn, replication_slot);
+}
+
 /*
  * Split argument into old_dir and new_dir and append to tablespace mapping
  * list.
@@ -647,6 +663,13 @@ PrepareLogStreamer(void)
 								   false, false))
 			exit(1);
 
+		/*
+		 * Only register atexit function if create_slot is true. Temporary
+		 * slots go away with connection
+		 */
+		if (create_slot)
+			atexit(drop_lingering_replication_slot_atexit);
+
 		if (verbose)
 		{
 			if (temp_replication_slot)
diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index 2442131e179..b6a31703262 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -828,9 +828,51 @@ $node->command_fails_like(
 	qr/replication slot "slot0" already exists/,
 	'pg_basebackup fails with --create-slot --slot and a previously existing slot'
 );
+is( $node->safe_psql(
+		'postgres',
+		q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'slot0'}
+	),
+	'slot0',
+	'previously existing slot is not dropped when --create-slot fails');
+
+# The slot is created before BASE_BACKUP is sent, so a failure of
+# BASE_BACKUP itself must not leave a slot created by --create-slot behind.
+$node->command_fails_like(
+	[
+		@pg_basebackup_defs,
+		'--pgdata' => "$tempdir/backupxs_slot2",
+		'--create-slot',
+		'--slot' => 'slot2',
+		'--manifest-checksums' => 'bogus'
+	],
+	qr/unrecognized checksum algorithm/,
+	'pg_basebackup --create-slot fails when BASE_BACKUP fails');
+is( $node->safe_psql(
+		'postgres',
+		q{SELECT count(*) FROM pg_replication_slots WHERE slot_name = 'slot2'}
+	),
+	'0',
+	'slot created by failed pg_basebackup --create-slot is dropped');
 
 $node->safe_psql('postgres',
 	q{SELECT * FROM pg_create_physical_replication_slot('slot1')});
+
+# Without --create-slot, a failed backup must leave the given slot alone.
+$node->command_fails_like(
+	[
+		@pg_basebackup_defs,
+		'--pgdata' => "$tempdir/backupxs_slot1_fail",
+		'--slot' => 'slot1',
+		'--manifest-checksums' => 'bogus'
+	],
+	qr/unrecognized checksum algorithm/,
+	'pg_basebackup --slot fails when BASE_BACKUP fails');
+is( $node->safe_psql(
+		'postgres',
+		q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'slot1'}
+	),
+	'slot1',
+	'existing slot is not dropped when pg_basebackup --slot fails');
 my $lsn = $node->safe_psql('postgres',
 	q{SELECT restart_lsn FROM pg_replication_slots WHERE slot_name = 'slot1'}
 );
-- 
2.56.0

