From 91c6bb335e4aa09e7e631f1cd8786459d6675b93 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 26 Mar 2025 19:03:50 +0900
Subject: [PATCH v5-PG17-1] Stabilize 035_standby_logical_decoding.pl

This test tries to invalidate slots on standby server, by running VACUUM on
primary and discarding needed tuples for slots. The problem is that
xl_running_xacts records are sotimetimes generated while testing, it advances
the catalog_xmin so that the invalidation might not happen in some cases.

The fix is to skip using the active slots for some testcases.
---
 .../t/035_standby_logical_decoding.pl         | 171 +++++++++---------
 1 file changed, 86 insertions(+), 85 deletions(-)

diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl
index aeb79f51e71..1f3ae86c556 100644
--- a/src/test/recovery/t/035_standby_logical_decoding.pl
+++ b/src/test/recovery/t/035_standby_logical_decoding.pl
@@ -44,27 +44,50 @@ sub wait_for_xmins
 # Create the required logical slots on standby.
 sub create_logical_slots
 {
-	my ($node, $slot_prefix) = @_;
+	my ($node, $slot_prefix, $needs_active_slot) = @_;
 
-	my $active_slot = $slot_prefix . 'activeslot';
 	my $inactive_slot = $slot_prefix . 'inactiveslot';
 	$node->create_logical_slot_on_standby($node_primary, qq($inactive_slot),
 		'testdb');
-	$node->create_logical_slot_on_standby($node_primary, qq($active_slot),
-		'testdb');
+
+	if ($needs_active_slot)
+	{
+		my $active_slot = $slot_prefix . 'activeslot';
+
+		$node->create_logical_slot_on_standby($node_primary, qq($active_slot),
+			'testdb');
+	}
+}
+
+# Checks the existence of the active slot. Returns the name if found, otherwise
+# undef.
+sub active_slot_exists
+{
+	my ($slot_prefix) = @_;
+
+	my $active_slot = $slot_prefix . 'activeslot';
+	my $active_slot_info = $node_standby->slot($active_slot);
+
+	return $active_slot_info->{'plugin'} eq '' ? undef : $active_slot;
 }
 
 # Drop the logical slots on standby.
 sub drop_logical_slots
 {
 	my ($slot_prefix) = @_;
-	my $active_slot = $slot_prefix . 'activeslot';
 	my $inactive_slot = $slot_prefix . 'inactiveslot';
 
 	$node_standby->psql('postgres',
 		qq[SELECT pg_drop_replication_slot('$inactive_slot')]);
-	$node_standby->psql('postgres',
-		qq[SELECT pg_drop_replication_slot('$active_slot')]);
+
+	# Drops the active slot as well, if exists
+	if (active_slot_exists($slot_prefix))
+	{
+		my $active_slot = $slot_prefix . 'activeslot';
+
+		$node_standby->psql('postgres',
+				qq[SELECT pg_drop_replication_slot('$active_slot')]);
+	}
 }
 
 # Acquire one of the standby logical slots created by create_logical_slots().
@@ -173,40 +196,47 @@ sub check_slots_conflict_reason
 {
 	my ($slot_prefix, $reason) = @_;
 
-	my $active_slot = $slot_prefix . 'activeslot';
 	my $inactive_slot = $slot_prefix . 'inactiveslot';
 
-	$res = $node_standby->safe_psql(
-		'postgres', qq(
-			 select invalidation_reason from pg_replication_slots where slot_name = '$active_slot' and conflicting;)
-	);
-
-	is($res, "$reason", "$active_slot reason for conflict is $reason");
-
 	$res = $node_standby->safe_psql(
 		'postgres', qq(
 			 select invalidation_reason from pg_replication_slots where slot_name = '$inactive_slot' and conflicting;)
 	);
 
 	is($res, "$reason", "$inactive_slot reason for conflict is $reason");
+
+	if (active_slot_exists($slot_prefix))
+	{
+		my $active_slot = $slot_prefix . 'activeslot';
+
+		$res = $node_standby->safe_psql(
+			'postgres', qq(
+				select invalidation_reason from pg_replication_slots where slot_name = '$active_slot' and conflicting;)
+		);
+
+		is($res, "$reason", "$active_slot reason for conflict is $reason");
+	}
 }
 
-# Drop the slots, re-create them, change hot_standby_feedback,
-# check xmin and catalog_xmin values, make slot active and reset stat.
+# Create slots, change hot_standby_feedback, check xmin and catalog_xmin
+# values, make slot active and reset stat.
 sub reactive_slots_change_hfs_and_wait_for_xmins
 {
-	my ($previous_slot_prefix, $slot_prefix, $hsf, $invalidated) = @_;
+	my ($previous_slot_prefix, $slot_prefix, $hsf, $invalidated, $needs_active_slot) = @_;
 
 	# drop the logical slots
 	drop_logical_slots($previous_slot_prefix);
 
 	# create the logical slots
-	create_logical_slots($node_standby, $slot_prefix);
+	create_logical_slots($node_standby, $slot_prefix, $needs_active_slot);
 
 	change_hot_standby_feedback_and_wait_for_xmins($hsf, $invalidated);
 
-	$handle =
-	  make_slot_active($node_standby, $slot_prefix, 1, \$stdout, \$stderr);
+	if ($needs_active_slot)
+	{
+		$handle =
+		  make_slot_active($node_standby, $slot_prefix, 1, \$stdout, \$stderr);
+	}
 
 	# reset stat: easier to check for confl_active_logicalslot in pg_stat_database_conflicts
 	$node_standby->psql('testdb', q[select pg_stat_reset();]);
@@ -217,7 +247,6 @@ sub check_for_invalidation
 {
 	my ($slot_prefix, $log_start, $test_name) = @_;
 
-	my $active_slot = $slot_prefix . 'activeslot';
 	my $inactive_slot = $slot_prefix . 'inactiveslot';
 
 	# message should be issued
@@ -226,18 +255,23 @@ sub check_for_invalidation
 			$log_start),
 		"inactiveslot slot invalidation is logged $test_name");
 
-	ok( $node_standby->log_contains(
-			"invalidating obsolete replication slot \"$active_slot\"",
-			$log_start),
-		"activeslot slot invalidation is logged $test_name");
-
-	# Verify that pg_stat_database_conflicts.confl_active_logicalslot has been updated
-	ok( $node_standby->poll_query_until(
-			'postgres',
-			"select (confl_active_logicalslot = 1) from pg_stat_database_conflicts where datname = 'testdb'",
-			't'),
-		'confl_active_logicalslot updated'
-	) or die "Timed out waiting confl_active_logicalslot to be updated";
+	if (active_slot_exists($slot_prefix))
+	{
+		my $active_slot = $slot_prefix . 'activeslot';
+
+		ok( $node_standby->log_contains(
+				"invalidating obsolete replication slot \"$active_slot\"",
+				$log_start),
+			"activeslot slot invalidation is logged $test_name");
+
+		# Verify that pg_stat_database_conflicts.confl_active_logicalslot has been updated
+		ok( $node_standby->poll_query_until(
+				'postgres',
+				"select (confl_active_logicalslot = 1) from pg_stat_database_conflicts where datname = 'testdb'",
+				't'),
+			'confl_active_logicalslot updated'
+		) or die "Timed out waiting confl_active_logicalslot to be updated";
+	}
 }
 
 # Launch $sql query, wait for a new snapshot that has a newer horizon and
@@ -250,7 +284,8 @@ sub check_for_invalidation
 # seeing a xl_running_xacts that would advance an active replication slot's
 # catalog_xmin.  Advancing the active replication slot's catalog_xmin
 # would break some tests that expect the active slot to conflict with
-# the catalog xmin horizon.
+# the catalog xmin horizon. We ensure that active replication slots are not
+# created for tests that might produce this race condition though.
 sub wait_until_vacuum_can_remove
 {
 	my ($vac_option, $sql, $to_vac) = @_;
@@ -389,7 +424,7 @@ $node_standby->safe_psql('postgres',
 ##################################################
 
 # create the logical slots
-create_logical_slots($node_standby, 'behaves_ok_');
+create_logical_slots($node_standby, 'behaves_ok_', 1);
 
 $node_primary->safe_psql('testdb',
 	qq[CREATE TABLE decoding_test(x integer, y text);]);
@@ -543,17 +578,13 @@ $node_subscriber->stop;
 # launch a vacuum full on pg_class with hot_standby_feedback turned off on
 # the standby.
 reactive_slots_change_hfs_and_wait_for_xmins('behaves_ok_', 'vacuum_full_',
-	0, 1);
+	0, 1, 0);
 
 # Ensure that replication slot stats are not empty before triggering the
 # conflict.
 $node_primary->safe_psql('testdb',
 	qq[INSERT INTO decoding_test(x,y) SELECT 100,'100';]);
 
-$node_standby->poll_query_until('testdb',
-	qq[SELECT total_txns > 0 FROM pg_stat_replication_slots WHERE slot_name = 'vacuum_full_activeslot']
-) or die "replication slot stats of vacuum_full_activeslot not updated";
-
 # This should trigger the conflict
 wait_until_vacuum_can_remove(
 	'full', 'CREATE TABLE conflict_test(x integer, y text);
@@ -567,22 +598,6 @@ check_for_invalidation('vacuum_full_', 1, 'with vacuum FULL on pg_class');
 # Verify reason for conflict is 'rows_removed' in pg_replication_slots
 check_slots_conflict_reason('vacuum_full_', 'rows_removed');
 
-# Ensure that replication slot stats are not removed after invalidation.
-is( $node_standby->safe_psql(
-		'testdb',
-		qq[SELECT total_txns > 0 FROM pg_stat_replication_slots WHERE slot_name = 'vacuum_full_activeslot']
-	),
-	't',
-	'replication slot stats not removed after invalidation');
-
-$handle =
-  make_slot_active($node_standby, 'vacuum_full_', 0, \$stdout, \$stderr);
-
-# We are not able to read from the slot as it has been invalidated
-check_pg_recvlogical_stderr($handle,
-	"can no longer get changes from replication slot \"vacuum_full_activeslot\""
-);
-
 # Turn hot_standby_feedback back on
 change_hot_standby_feedback_and_wait_for_xmins(1, 1);
 
@@ -602,7 +617,7 @@ check_slots_conflict_reason('vacuum_full_', 'rows_removed');
 my $restart_lsn = $node_standby->safe_psql(
 	'postgres',
 	"SELECT restart_lsn FROM pg_replication_slots
-		WHERE slot_name = 'vacuum_full_activeslot' AND conflicting;"
+		WHERE slot_name = 'vacuum_full_inactiveslot' AND conflicting;"
 );
 
 chomp($restart_lsn);
@@ -641,7 +656,7 @@ my $logstart = -s $node_standby->logfile;
 # launch a vacuum on pg_class with hot_standby_feedback turned off on the
 # standby.
 reactive_slots_change_hfs_and_wait_for_xmins('vacuum_full_', 'row_removal_',
-	0, 1);
+	0, 1, 0);
 
 # This should trigger the conflict
 wait_until_vacuum_can_remove(
@@ -656,14 +671,6 @@ check_for_invalidation('row_removal_', $logstart, 'with vacuum on pg_class');
 # Verify reason for conflict is 'rows_removed' in pg_replication_slots
 check_slots_conflict_reason('row_removal_', 'rows_removed');
 
-$handle =
-  make_slot_active($node_standby, 'row_removal_', 0, \$stdout, \$stderr);
-
-# We are not able to read from the slot as it has been invalidated
-check_pg_recvlogical_stderr($handle,
-	"can no longer get changes from replication slot \"row_removal_activeslot\""
-);
-
 ##################################################
 # Recovery conflict: Same as Scenario 2 but on a shared catalog table
 # Scenario 3: conflict due to row removal with hot_standby_feedback off.
@@ -676,7 +683,7 @@ $logstart = -s $node_standby->logfile;
 # create/drop a role and launch a vacuum on pg_authid with
 # hot_standby_feedback turned off on the standby.
 reactive_slots_change_hfs_and_wait_for_xmins('row_removal_',
-	'shared_row_removal_', 0, 1);
+	'shared_row_removal_', 0, 1, 0);
 
 # Trigger the conflict
 wait_until_vacuum_can_remove(
@@ -692,14 +699,6 @@ check_for_invalidation('shared_row_removal_', $logstart,
 # Verify reason for conflict is 'rows_removed' in pg_replication_slots
 check_slots_conflict_reason('shared_row_removal_', 'rows_removed');
 
-$handle = make_slot_active($node_standby, 'shared_row_removal_', 0, \$stdout,
-	\$stderr);
-
-# We are not able to read from the slot as it has been invalidated
-check_pg_recvlogical_stderr($handle,
-	"can no longer get changes from replication slot \"shared_row_removal_activeslot\""
-);
-
 ##################################################
 # Recovery conflict: Same as Scenario 2 but on a non catalog table
 # Scenario 4: No conflict expected.
@@ -747,6 +746,12 @@ is( $node_standby->safe_psql(
 	'f',
 	'Logical slots are reported as non conflicting');
 
+my $tmp = $node_standby->safe_psql(
+		'postgres',
+		q[select slot_name, conflicting from pg_replication_slots
+			where slot_type = 'logical']);
+print $tmp;
+
 # Turn hot_standby_feedback back on
 change_hot_standby_feedback_and_wait_for_xmins(1, 0);
 
@@ -764,7 +769,7 @@ $logstart = -s $node_standby->logfile;
 # One way to produce recovery conflict is to trigger an on-access pruning
 # on a relation marked as user_catalog_table.
 reactive_slots_change_hfs_and_wait_for_xmins('no_conflict_', 'pruning_', 0,
-	0);
+	0, 0);
 
 # This should trigger the conflict
 $node_primary->safe_psql('testdb',
@@ -786,10 +791,6 @@ check_slots_conflict_reason('pruning_', 'rows_removed');
 
 $handle = make_slot_active($node_standby, 'pruning_', 0, \$stdout, \$stderr);
 
-# We are not able to read from the slot as it has been invalidated
-check_pg_recvlogical_stderr($handle,
-	"can no longer get changes from replication slot \"pruning_activeslot\"");
-
 # Turn hot_standby_feedback back on
 change_hot_standby_feedback_and_wait_for_xmins(1, 1);
 
@@ -805,7 +806,7 @@ $logstart = -s $node_standby->logfile;
 drop_logical_slots('pruning_');
 
 # create the logical slots
-create_logical_slots($node_standby, 'wal_level_');
+create_logical_slots($node_standby, 'wal_level_', 1);
 
 $handle =
   make_slot_active($node_standby, 'wal_level_', 1, \$stdout, \$stderr);
@@ -858,7 +859,7 @@ check_pg_recvlogical_stderr($handle,
 drop_logical_slots('wal_level_');
 
 # create the logical slots
-create_logical_slots($node_standby, 'drop_db_');
+create_logical_slots($node_standby, 'drop_db_', 1);
 
 $handle = make_slot_active($node_standby, 'drop_db_', 1, \$stdout, \$stderr);
 
@@ -922,14 +923,14 @@ $node_cascading_standby->append_conf(
 $node_cascading_standby->start;
 
 # create the logical slots
-create_logical_slots($node_standby, 'promotion_');
+create_logical_slots($node_standby, 'promotion_', 1);
 
 # Wait for the cascading standby to catchup before creating the slots
 $node_standby->wait_for_replay_catchup($node_cascading_standby,
 	$node_primary);
 
 # create the logical slots on the cascading standby too
-create_logical_slots($node_cascading_standby, 'promotion_');
+create_logical_slots($node_cascading_standby, 'promotion_', 1);
 
 # Make slots actives
 $handle =
-- 
2.43.5

