From bc40a1d0d470917e44d06caab52b482e907f2d78 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Mon, 6 Oct 2025 16:20:45 +0300
Subject: [PATCH 1/2] Newly created replication slot may be invalidated by
 checkpoint

Commit 2090edc6f32f652a2c995ca5f7e65748ae1e4c5d introduced a change
that the minimal restart_lsn is obtained at the start of checkpoint
creation. If a replication slot is created and performs a WAL
reservation concurrently, the WAL segment contains the new slot's
restart_lsn could be removed by the ongoing checkpoint. Add a perl
test to reproduce this scenario.

Author: suyu.cmj <mengjuan.cmj@alibaba-inc.com>
Author: Vitaly Davydov <v.davydov@postgrespro.ru>
---
 src/backend/replication/slot.c                |   3 +
 .../recovery/t/049_invalidate_new_slot.pl     | 188 ++++++++++++++++++
 2 files changed, 191 insertions(+)
 create mode 100644 src/test/recovery/t/049_invalidate_new_slot.pl

diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index b9e2b115dab..ee0ab2d8c83 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -57,6 +57,7 @@
 #include "utils/builtins.h"
 #include "utils/guc_hooks.h"
 #include "utils/varlena.h"
+#include "utils/injection_point.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -1443,6 +1444,8 @@ ReplicationSlotReserveWal(void)
 		slot->data.restart_lsn = restart_lsn;
 		SpinLockRelease(&slot->mutex);
 
+		INJECTION_POINT("slot-reserve-wal-delay");
+
 		/* prevent WAL removal as fast as possible */
 		ReplicationSlotsComputeRequiredLSN();
 
diff --git a/src/test/recovery/t/049_invalidate_new_slot.pl b/src/test/recovery/t/049_invalidate_new_slot.pl
new file mode 100644
index 00000000000..a69df941762
--- /dev/null
+++ b/src/test/recovery/t/049_invalidate_new_slot.pl
@@ -0,0 +1,188 @@
+# This test checks that the new slot maybe invalidated by checkpoint
+#
+
+use strict;
+use warnings;
+use Config;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my $res;
+
+# Setup primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init(allows_streaming => 1);
+$node->append_conf('postgresql.conf', q{wal_level = logical});
+$node->start();
+$node->safe_psql('postgres', q{create extension injection_points;});
+
+my $createslot = $node->background_psql('postgres');
+my $checkpoint = $node->background_psql('postgres');
+
+sub physreplslot_start_create()
+{
+	$createslot->query_until(
+		qr/start-create-slot/,
+		q(\echo start-create-slot
+		select pg_create_physical_replication_slot('standby1', true);
+	));
+}
+
+sub logreplslot_start_create()
+{
+	$createslot->query_until(
+		qr/start-create-slot/,
+		q(\echo start-create-slot
+		select pg_log_standby_snapshot();
+		select pg_create_logical_replication_slot('standby1', 'pgoutput');
+	));
+}
+
+sub physreplslot_create()
+{
+	$node->safe_psql('postgres', q{
+		select pg_create_physical_replication_slot('standby1', true)
+	});
+}
+
+sub checkpoint_start()
+{
+	$checkpoint->query_until(
+		qr/start-checkpoint/,
+		q(\echo start-checkpoint
+		checkpoint;
+	));
+}
+
+sub injection_point_attach($$)
+{
+	my ($injectpname, $event) = @_;
+
+	$node->safe_psql('postgres', "select injection_points_attach('$injectpname','$event')");
+}
+
+sub injection_point_detach($)
+{
+	my $injectpname = shift;
+
+	$node->safe_psql('postgres', "select injection_points_detach('$injectpname');");
+}
+
+sub injection_point_wait($$)
+{
+	my ($subsystem, $injectpname) = @_;
+
+	note("waiting for $injectpname injection point");
+	$node->wait_for_event($subsystem, $injectpname);
+	note("injection_point $injectpname is reached");
+}
+
+sub injection_point_wakeup($)
+{
+	my $injectpname = shift;
+
+	$node->safe_psql('postgres', "select injection_points_wakeup('$injectpname');");
+}
+
+sub get_slot_invalidation_state($)
+{
+	my $slotname = shift;
+
+	return $node->safe_psql('postgres',
+		"select invalidation_reason from pg_replication_slots where slot_name='$slotname'");
+}
+
+# ------------------------------------------------------------------------------
+# Test: start physical slot creation before checkpoint (before new redo lsn assignment)
+# ------------------------------------------------------------------------------
+
+injection_point_attach('slot-reserve-wal-delay', 'wait');
+injection_point_attach('checkpoint-before-old-wal-removal', 'wait');
+
+physreplslot_start_create();
+$node->advance_wal(3);
+checkpoint_start();
+injection_point_wait('client backend', 'slot-reserve-wal-delay');
+injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal');
+injection_point_wakeup('slot-reserve-wal-delay');
+injection_point_wakeup('checkpoint-before-old-wal-removal');
+
+eval { $createslot->quit(); };
+eval { $checkpoint->quit(); };
+
+is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (1)");
+
+injection_point_detach('slot-reserve-wal-delay');
+injection_point_detach('checkpoint-before-old-wal-removal');
+$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')");
+
+# ------------------------------------------------------------------------------
+# Test: start physical slot creation during checkpoint (after new redo lsn assignment)
+# ------------------------------------------------------------------------------
+
+injection_point_attach('checkpoint-before-old-wal-removal', 'wait');
+
+$node->advance_wal(3);
+checkpoint_start();
+injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal');
+physreplslot_create();
+injection_point_wakeup('checkpoint-before-old-wal-removal');
+
+eval { $checkpoint->quit(); };
+
+is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (2)");
+
+injection_point_detach('checkpoint-before-old-wal-removal');
+$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')");
+
+# ------------------------------------------------------------------------------
+# Test: start logical slot creation before checkpoint (before new redo lsn assignment)
+# ------------------------------------------------------------------------------
+
+injection_point_attach('slot-reserve-wal-delay', 'wait');
+injection_point_attach('checkpoint-before-old-wal-removal', 'wait');
+
+logreplslot_start_create();
+$node->advance_wal(3);
+checkpoint_start();
+injection_point_wait('client backend', 'slot-reserve-wal-delay');
+injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal');
+injection_point_wakeup('slot-reserve-wal-delay');
+injection_point_wakeup('checkpoint-before-old-wal-removal');
+
+eval { $createslot->quit(); };
+eval { $checkpoint->quit(); };
+
+is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (3)");
+
+injection_point_detach('slot-reserve-wal-delay');
+injection_point_detach('checkpoint-before-old-wal-removal');
+$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')");
+
+# ------------------------------------------------------------------------------
+# Test: start logical slot creation during checkpoint (after new redo lsn assignment)
+# ------------------------------------------------------------------------------
+
+injection_point_attach('checkpoint-before-old-wal-removal', 'wait');
+
+$node->advance_wal(3);
+checkpoint_start();
+injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal');
+logreplslot_start_create();
+injection_point_wakeup('checkpoint-before-old-wal-removal');
+
+eval { $createslot->quit(); };
+eval { $checkpoint->quit(); };
+
+is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (4)");
+
+injection_point_detach('checkpoint-before-old-wal-removal');
+$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')");
+
+done_testing();
-- 
2.34.1

