From 1f2e51e3c7c2042e4cb184b9a9030c1353de298e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 20 Aug 2025 15:00:04 +0900
Subject: [PATCH] Fix assertion failure with replication slot release in
 single-user mode

Some replication slot manipulations (logical decoding via SQL,
advancing) were failing an assertion when releasing a slot in
single-user mode, because active_pid was not set in a ReplicationSlot
when its slot is acquired.

ReplicationSlotAcquire() has some logic to be able to work with the
single-user mode.  This commit sets ReplicationSlot->active_pid to
MyProcPid, to let the slot-related logic fall-through, considering the
single process as the one holding the slot.

Some TAP tests are added for various replication slot functions with the
single-user mode, while on it, for slot creation, drop, advancing, copy
and logical decoding with multiple slot types (temporary, physical vs
logical).  These tests are skipped on Windows, as direct calls of
postgres --single would fail on permission failures.  There is no
platform-specific behavior that needs to be checked, so living with this
restriction should be fine.  The CI is OK with that, now let's see what
the buildfarm tells.

Author: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Reviewed-by: Paul A. Jungwirth <pj@illuminatedcomputing.com>
Reviewed-by: Mutaamba Maasha <maasha@gmail.com>
Discussion: https://postgr.es/m/OSCPR01MB14966ED588A0328DAEBE8CB25F5FA2@OSCPR01MB14966.jpnprd01.prod.outlook.com
Backpatch-through: 13
---
 src/backend/replication/slot.c                |  2 +-
 src/test/modules/test_misc/Makefile           |  3 +-
 src/test/modules/test_misc/meson.build        |  1 +
 .../test_misc/t/008_replslot_single_user.pl   | 95 +++++++++++++++++++
 4 files changed, 99 insertions(+), 2 deletions(-)
 create mode 100644 src/test/modules/test_misc/t/008_replslot_single_user.pl

diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 8605776ad8..fd0fdb96d4 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -653,7 +653,7 @@ retry:
 	}
 	else
 	{
-		active_pid = MyProcPid;
+		s->active_pid = active_pid = MyProcPid;
 		ReplicationSlotSetInactiveSince(s, 0, true);
 	}
 	LWLockRelease(ReplicationSlotControlLock);
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 919a25fc67..399b9094a3 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,7 +2,8 @@
 
 TAP_TESTS = 1
 
-EXTRA_INSTALL=src/test/modules/injection_points
+EXTRA_INSTALL=src/test/modules/injection_points \
+	contrib/test_decoding
 
 export enable_injection_points
 
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 9c50de7efb..6b1e730bf4 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -16,6 +16,7 @@ tests += {
       't/005_timeouts.pl',
       't/006_signal_autovacuum.pl',
       't/007_catcache_inval.pl',
+      't/008_replslot_single_user.pl',
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/008_replslot_single_user.pl b/src/test/modules/test_misc/t/008_replslot_single_user.pl
new file mode 100644
index 0000000000..796700d621
--- /dev/null
+++ b/src/test/modules/test_misc/t/008_replslot_single_user.pl
@@ -0,0 +1,95 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test manipulations of replication slots with the single-user mode.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Skip the tests on Windows, as single-user mode would fail on permission
+# failure with privileged accounts.
+if ($windows_os)
+{
+	plan skip_all => 'this test is not supported by this platform';
+}
+
+# Run set of queries in single-user mode.
+sub test_single_mode
+{
+	my ($node, $queries, $testname) = @_;
+
+	my $result = run_log(
+		[
+			'postgres', '--single', '-F',
+			'-c' => 'exit_on_error=true',
+			'-D' => $node->data_dir,
+			'postgres'
+		],
+		'<' => \$queries);
+
+	ok($result, $testname);
+}
+
+my $slot_logical = 'slot_logical';
+my $slot_physical = 'slot_physical';
+
+# Initialize a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(allows_streaming => "logical");
+$node->start;
+
+# Define initial table
+$node->safe_psql('postgres', "CREATE TABLE foo (id int)");
+
+$node->stop;
+
+test_single_mode(
+	$node,
+	"SELECT pg_create_logical_replication_slot('$slot_logical', 'test_decoding')",
+	"logical slot creation");
+test_single_mode(
+	$node,
+	"SELECT pg_create_physical_replication_slot('$slot_physical', true)",
+	"physical slot creation");
+test_single_mode(
+	$node,
+	"SELECT pg_create_physical_replication_slot('slot_tmp', true, true)",
+	"temporary physical slot creation");
+
+test_single_mode(
+	$node, qq(
+INSERT INTO foo VALUES (1);
+SELECT pg_logical_slot_get_changes('$slot_logical', NULL, NULL);
+),
+	"logical decoding");
+
+test_single_mode(
+	$node,
+	"SELECT pg_replication_slot_advance('$slot_logical', pg_current_wal_lsn())",
+	"logical slot advance");
+test_single_mode(
+	$node,
+	"SELECT pg_replication_slot_advance('$slot_physical', pg_current_wal_lsn())",
+	"physical slot advance");
+
+test_single_mode(
+	$node,
+	"SELECT pg_copy_logical_replication_slot('$slot_logical', 'slot_log_copy')",
+	"logical slot copy");
+test_single_mode(
+	$node,
+	"SELECT pg_copy_physical_replication_slot('$slot_physical', 'slot_phy_copy')",
+	"physical slot copy");
+
+test_single_mode(
+	$node,
+	"SELECT pg_drop_replication_slot('$slot_logical')",
+	"logical slot drop");
+test_single_mode(
+	$node,
+	"SELECT pg_drop_replication_slot('$slot_physical')",
+	"physical slot drop");
+
+done_testing();
-- 
2.39.5

