From 43d8e1a7fa9e8ea0870fc77a1e4b4301187be987 Mon Sep 17 00:00:00 2001
From: Shveta Malik <shveta.malik@gmail.com>
Date: Wed, 27 Dec 2023 10:22:39 +0530
Subject: [PATCH v1] Track conflicting_cause in pg_replication_slots

This patch changes existing 'conflicting' field to 'conflicting_cause'
in pg_replication_slots. This new field indicates cause if this logical
slot conflicted with recovery (and so is now invalidated).
It is always NULL for physical slots, as well as for those logical slots
which are not invalidated. Possible values are:

    wal_removed = required WAL has been removed.
    rows_removed = required rows have been removed.
    wal_level_insufficient = wal_level insufficient on the primary server.
---
 doc/src/sgml/system-views.sgml                | 28 +++++++++--
 src/backend/catalog/system_views.sql          |  2 +-
 src/backend/replication/slotfuncs.c           | 22 ++++++--
 src/bin/pg_upgrade/info.c                     |  4 +-
 src/include/catalog/pg_proc.dat               |  4 +-
 .../t/035_standby_logical_decoding.pl         | 50 ++++++++++++++++---
 src/test/regress/expected/rules.out           |  4 +-
 7 files changed, 92 insertions(+), 22 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 0ef1745631..f02c23ba52 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2525,11 +2525,29 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>conflicting</structfield> <type>bool</type>
-      </para>
-      <para>
-       True if this logical slot conflicted with recovery (and so is now
-       invalidated). Always NULL for physical slots.
+       <structfield>conflicting_cause</structfield> <type>text</type>
+      </para>
+      <para>
+       Cause if this logical slot conflicted with recovery (and so is now
+       invalidated). It is always NULL for physical slots, as well as for
+       those logical slots which are not invalidated. Possible values are:
+        <itemizedlist spacing="compact">
+         <listitem>
+          <para>
+           <literal>wal_removed</literal> = required WAL has been removed.
+          </para>
+         </listitem>
+         <listitem>
+          <para>
+           <literal>rows_removed</literal> = required rows have been removed.
+          </para>
+         </listitem>
+         <listitem>
+          <para>
+           <literal>wal_level_insufficient</literal> = wal_level insufficient on the primary server.
+          </para>
+         </listitem>
+        </itemizedlist>
       </para></entry>
      </row>
     </tbody>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 058fc47c91..ba9f497e78 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,7 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflicting
+            L.conflicting_cause
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 4b694a03d0..8e3d27adc8 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -406,10 +406,24 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			nulls[i++] = true;
 		else
 		{
-			if (slot_contents.data.invalidated != RS_INVAL_NONE)
-				values[i++] = BoolGetDatum(true);
-			else
-				values[i++] = BoolGetDatum(false);
+			switch (slot_contents.data.invalidated)
+			{
+				case RS_INVAL_NONE:
+					nulls[i++] = true;
+					break;
+
+				case RS_INVAL_WAL_REMOVED:
+					values[i++] = CStringGetTextDatum("wal_removed");
+					break;
+
+				case RS_INVAL_HORIZON:
+					values[i++] = CStringGetTextDatum("rows_removed");
+					break;
+
+				case RS_INVAL_WAL_LEVEL:
+					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					break;
+			}
 		}
 
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 4878aa22bf..edff3107d3 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -662,13 +662,13 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * removed.
 	 */
 	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
-							"%s as caught_up, conflicting as invalid "
+							"%s as caught_up, conflicting_cause IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
 							"database = current_database() AND "
 							"temporary IS FALSE;",
 							live_check ? "FALSE" :
-							"(CASE WHEN conflicting THEN FALSE "
+							"(CASE WHEN conflicting_cause IS NOT NULL THEN FALSE "
 							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
 							"END)");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9052f5262a..de9a94a2bb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11115,9 +11115,9 @@
   proname => 'pg_get_replication_slots', prorows => '10', proisstrict => 'f',
   proretset => 't', provolatile => 's', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,bool}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text}',
   proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{slot_name,plugin,slot_type,datoid,temporary,active,active_pid,xmin,catalog_xmin,restart_lsn,confirmed_flush_lsn,wal_status,safe_wal_size,two_phase,conflicting}',
+  proargnames => '{slot_name,plugin,slot_type,datoid,temporary,active,active_pid,xmin,catalog_xmin,restart_lsn,confirmed_flush_lsn,wal_status,safe_wal_size,two_phase,conflicting_cause}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl
index 5d7c278d01..05ff94d121 100644
--- a/src/test/recovery/t/035_standby_logical_decoding.pl
+++ b/src/test/recovery/t/035_standby_logical_decoding.pl
@@ -177,7 +177,8 @@ sub check_slots_conflicting_status
 	{
 		$res = $node_standby->safe_psql(
 			'postgres', qq(
-				 select bool_and(conflicting) from pg_replication_slots;));
+				 select bool_and(conflicting) from
+				 (select conflicting_cause is not NULL as conflicting from pg_replication_slots);));
 
 		is($res, 't', "Logical slots are reported as conflicting");
 	}
@@ -185,12 +186,34 @@ sub check_slots_conflicting_status
 	{
 		$res = $node_standby->safe_psql(
 			'postgres', qq(
-				select bool_or(conflicting) from pg_replication_slots;));
+				select bool_or(conflicting) from
+				(select conflicting_cause is not NULL as conflicting from pg_replication_slots);));
 
 		is($res, 'f', "Logical slots are reported as non conflicting");
 	}
 }
 
+# Check conflicting_cause in pg_replication_slots.
+sub check_slots_conflicting_cause
+{
+	my ($slot_prefix, $cause) = @_;
+
+	my $active_slot = $slot_prefix . 'activeslot';
+	my $inactive_slot = $slot_prefix . 'inactiveslot';
+
+	$res = $node_standby->safe_psql(
+		'postgres', qq(
+			 select conflicting_cause from pg_replication_slots where slot_name = '$active_slot';));
+
+	is($res, "$cause", "$active_slot conflicting cause is $cause");
+
+	$res = $node_standby->safe_psql(
+		'postgres', qq(
+			 select conflicting_cause from pg_replication_slots where slot_name = '$inactive_slot';));
+
+	is($res, "$cause", "$inactive_slot conflicting cause is $cause");
+}
+
 # Drop the slots, re-create them, 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
@@ -260,13 +283,13 @@ $node_primary->safe_psql('testdb',
 	qq[SELECT * FROM pg_create_physical_replication_slot('$primary_slotname');]
 );
 
-# Check conflicting is NULL for physical slot
+# Check conflicting_cause is NULL for physical slot
 $res = $node_primary->safe_psql(
 	'postgres', qq[
-		 SELECT conflicting is null FROM pg_replication_slots where slot_name = '$primary_slotname';]
+		 SELECT conflicting_cause is null FROM pg_replication_slots where slot_name = '$primary_slotname';]
 );
 
-is($res, 't', "Physical slot reports conflicting as NULL");
+is($res, 't', "Physical slot reports conflicting_cause as NULL");
 
 my $backup_name = 'b1';
 $node_primary->backup($backup_name);
@@ -486,6 +509,9 @@ check_for_invalidation('vacuum_full_', 1, 'with vacuum FULL on pg_class');
 # Verify slots are reported as conflicting in pg_replication_slots
 check_slots_conflicting_status(1);
 
+# Verify conflicting_cause is 'rows_removed' in pg_replication_slots
+check_slots_conflicting_cause('vacuum_full_', 'rows_removed');
+
 $handle =
   make_slot_active($node_standby, 'vacuum_full_', 0, \$stdout, \$stderr);
 
@@ -511,7 +537,7 @@ check_slots_conflicting_status(1);
 
 # Get the restart_lsn from an invalidated slot
 my $restart_lsn = $node_standby->safe_psql('postgres',
-	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'vacuum_full_activeslot' and conflicting is true;"
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'vacuum_full_activeslot' and conflicting_cause is not null;"
 );
 
 chomp($restart_lsn);
@@ -568,6 +594,9 @@ check_for_invalidation('row_removal_', $logstart, 'with vacuum on pg_class');
 # Verify slots are reported as conflicting in pg_replication_slots
 check_slots_conflicting_status(1);
 
+# Verify conflicting_cause is 'rows_removed' in pg_replication_slots
+check_slots_conflicting_cause('row_removal_', 'rows_removed');
+
 $handle =
   make_slot_active($node_standby, 'row_removal_', 0, \$stdout, \$stderr);
 
@@ -607,6 +636,9 @@ check_for_invalidation('shared_row_removal_', $logstart,
 # Verify slots are reported as conflicting in pg_replication_slots
 check_slots_conflicting_status(1);
 
+# Verify conflicting_cause is 'rows_removed' in pg_replication_slots
+check_slots_conflicting_cause('shared_row_removal_', 'rows_removed');
+
 $handle = make_slot_active($node_standby, 'shared_row_removal_', 0, \$stdout,
 	\$stderr);
 
@@ -696,6 +728,9 @@ check_for_invalidation('pruning_', $logstart, 'with on-access pruning');
 # Verify slots are reported as conflicting in pg_replication_slots
 check_slots_conflicting_status(1);
 
+# Verify conflicting_cause is 'rows_removed' in pg_replication_slots
+check_slots_conflicting_cause('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
@@ -740,6 +775,9 @@ check_for_invalidation('wal_level_', $logstart, 'due to wal_level');
 # Verify slots are reported as conflicting in pg_replication_slots
 check_slots_conflicting_status(1);
 
+# Verify conflicting_cause is 'wal_level_insufficient' in pg_replication_slots
+check_slots_conflicting_cause('wal_level_', 'wal_level_insufficient');
+
 $handle =
   make_slot_active($node_standby, 'wal_level_', 0, \$stdout, \$stderr);
 # We are not able to read from the slot as it requires wal_level >= logical on the primary server
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f645e8486b..95701f1175 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,8 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflicting
-   FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase, conflicting)
+    l.conflicting_cause
+   FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase, conflicting_cause)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
-- 
2.34.1

