From e05e86baf90a14efec29b0efcb5696504796af9b Mon Sep 17 00:00:00 2001
From: Mikhail Litsarev <m.litsarev@postgrespro.ru>
Date: Mon, 6 May 2024 15:23:52 +0300
Subject: [PATCH 1/2] Introduce pg_is_standby_requested().

Introduce a bit array SharedRecoveryDataFlags and
pg_is_standby_requested() function to distinguish
a replica from a regular instance in point in time recovery mode.
---
 src/backend/access/transam/xlogfuncs.c    | 14 +++++
 src/backend/access/transam/xlogrecovery.c | 75 ++++++++++++++++++-----
 src/include/access/xlogrecovery.h         |  1 +
 src/include/catalog/pg_proc.dat           |  4 ++
 4 files changed, 79 insertions(+), 15 deletions(-)

diff --git a/src/backend/access/transam/xlogfuncs.c b/src/backend/access/transam/xlogfuncs.c
index 92bdb17ed52..3ce934fcedb 100644
--- a/src/backend/access/transam/xlogfuncs.c
+++ b/src/backend/access/transam/xlogfuncs.c
@@ -747,3 +747,17 @@ pg_promote(PG_FUNCTION_ARGS)
 						   wait_seconds)));
 	PG_RETURN_BOOL(false);
 }
+
+/*
+ * Returns bool with a current value of StandbyModeIsRequested flag
+ * to distinguish a replica from a regular instance in a
+ * Point In Time Recovery (PITR) mode.
+ *
+ * Returns	true	if standby.signal file is found at startup process
+ * 			false	otherwise
+ */
+Datum
+pg_is_standby_requested(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(StandbyModeIsRequested());
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 29c5bec0847..5337c4974f0 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -170,13 +170,13 @@ static XLogRecPtr RedoStartLSN = InvalidXLogRecPtr;
 static TimeLineID RedoStartTLI = 0;
 
 /*
- * Local copy of SharedHotStandbyActive variable. False actually means "not
+ * Local copy of XLR_HOT_STANDBY_ACTIVE flag. False actually means "not
  * known, need to check the shared state".
  */
 static bool LocalHotStandbyActive = false;
 
 /*
- * Local copy of SharedPromoteIsTriggered variable. False actually means "not
+ * Local copy of XLR_PROMOTE_IS_TRIGGERED flag. False actually means "not
  * known, need to check the shared state".
  */
 static bool LocalPromoteIsTriggered = false;
@@ -298,22 +298,42 @@ static char *replay_image_masked = NULL;
 static char *primary_image_masked = NULL;
 
 
+/*
+ * The flag indicates if we allow hot standby queries to be
+ * run.  Protected by info_lck.
+ */
+#define	XLR_HOT_STANDBY_ACTIVE		0x01
+/*
+ * The flag indicates if a standby promotion has been
+ * triggered.  Protected by info_lck.
+ */
+#define	XLR_PROMOTE_IS_TRIGGERED	0x02
+/*
+ * The flag indicates if we're in a standby mode at
+ * start, while recovery mode is on.
+ */
+#define	XLR_STANDBY_MODE_REQUESTED	0x04
+
 /*
  * Shared-memory state for WAL recovery.
  */
 typedef struct XLogRecoveryCtlData
 {
 	/*
-	 * SharedHotStandbyActive indicates if we allow hot standby queries to be
-	 * run.  Protected by info_lck.
-	 */
-	bool		SharedHotStandbyActive;
-
-	/*
-	 * SharedPromoteIsTriggered indicates if a standby promotion has been
-	 * triggered.  Protected by info_lck.
+	 * This bit array is introduced to keep the following states:
+	 *
+	 *	--	XLR_HOT_STANDBY_ACTIVE indicates if we allow hot standby queries
+	 *		to be run. Protected by info_lck.
+	 *
+	 *	--	XLR_PROMOTE_IS_TRIGGERED indicates if a standby promotion
+	 *		has been triggered. Protected by info_lck.
+	 *
+	 * 	--	XLR_STANDBY_MODE_REQUESTED indicates if we're in a standby mode
+	 *		at start, while recovery mode is on. No info_lck protection.
+	 *
+	 *	and can be extended in future.
 	 */
-	bool		SharedPromoteIsTriggered;
+	uint32		SharedRecoveryDataFlags;
 
 	/*
 	 * recoveryWakeupLatch is used to wake up the startup process to continue
@@ -1082,10 +1102,17 @@ readRecoverySignalFile(void)
 
 	StandbyModeRequested = false;
 	ArchiveRecoveryRequested = false;
+	/*
+	 * There is no need to use Spinlock here because only the startup
+	 * process modifies the SharedRecoveryDataFlags bit here and
+	 * no other processes are reading it at that time.
+	 */
+	XLogRecoveryCtl->SharedRecoveryDataFlags &= ~XLR_STANDBY_MODE_REQUESTED;
 	if (standby_signal_file_found)
 	{
 		StandbyModeRequested = true;
 		ArchiveRecoveryRequested = true;
+		XLogRecoveryCtl->SharedRecoveryDataFlags |= XLR_STANDBY_MODE_REQUESTED;
 	}
 	else if (recovery_signal_file_found)
 	{
@@ -2259,7 +2286,7 @@ CheckRecoveryConsistency(void)
 		IsUnderPostmaster)
 	{
 		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
-		XLogRecoveryCtl->SharedHotStandbyActive = true;
+		XLogRecoveryCtl->SharedRecoveryDataFlags |= XLR_HOT_STANDBY_ACTIVE;
 		SpinLockRelease(&XLogRecoveryCtl->info_lck);
 
 		LocalHotStandbyActive = true;
@@ -4402,7 +4429,8 @@ PromoteIsTriggered(void)
 		return true;
 
 	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
-	LocalPromoteIsTriggered = XLogRecoveryCtl->SharedPromoteIsTriggered;
+	LocalPromoteIsTriggered =
+		(XLogRecoveryCtl->SharedRecoveryDataFlags & XLR_PROMOTE_IS_TRIGGERED) != 0;
 	SpinLockRelease(&XLogRecoveryCtl->info_lck);
 
 	return LocalPromoteIsTriggered;
@@ -4412,7 +4440,7 @@ static void
 SetPromoteIsTriggered(void)
 {
 	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
-	XLogRecoveryCtl->SharedPromoteIsTriggered = true;
+	XLogRecoveryCtl->SharedRecoveryDataFlags |= XLR_PROMOTE_IS_TRIGGERED;
 	SpinLockRelease(&XLogRecoveryCtl->info_lck);
 
 	/*
@@ -4512,7 +4540,8 @@ HotStandbyActive(void)
 	{
 		/* spinlock is essential on machines with weak memory ordering! */
 		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
-		LocalHotStandbyActive = XLogRecoveryCtl->SharedHotStandbyActive;
+		LocalHotStandbyActive =
+			(XLogRecoveryCtl->SharedRecoveryDataFlags & XLR_HOT_STANDBY_ACTIVE) != 0;
 		SpinLockRelease(&XLogRecoveryCtl->info_lck);
 
 		return LocalHotStandbyActive;
@@ -4530,6 +4559,22 @@ HotStandbyActiveInReplay(void)
 	return LocalHotStandbyActive;
 }
 
+/*
+ * It reports whether a standby.signal file is in the data directory
+ * at startup.
+ *
+ * This works in any process that's connected to shared memory.
+ */
+bool
+StandbyModeIsRequested(void)
+{
+	/*
+	 * Spinlock is not needed here because XLR_STANDBY_MODE_REQUESTED flag
+	 * can only be read after startup process is done.
+	 */
+	return (XLogRecoveryCtl->SharedRecoveryDataFlags & XLR_STANDBY_MODE_REQUESTED) != 0;
+}
+
 /*
  * Get latest redo apply position.
  *
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index c423464e8bc..ad94dd79629 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -144,6 +144,7 @@ extern TimestampTz GetLatestXTime(void);
 extern TimestampTz GetCurrentChunkReplayStartTime(void);
 extern XLogRecPtr GetCurrentReplayRecPtr(TimeLineID *replayEndTLI);
 
+extern bool StandbyModeIsRequested(void);
 extern bool PromoteIsTriggered(void);
 extern bool CheckPromoteSignal(void);
 extern void WakeupRecovery(void);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 134e3b22fd8..9bf97f81d61 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6519,6 +6519,10 @@
 { oid => '3810', descr => 'true if server is in recovery',
   proname => 'pg_is_in_recovery', provolatile => 'v', prorettype => 'bool',
   proargtypes => '', prosrc => 'pg_is_in_recovery' },
+{ oid => '8439',
+  descr => 'Is StandbyMode requested at startup while being in recovery mode',
+  proname => 'pg_is_standby_requested', provolatile => 'v', prorettype => 'bool',
+  proargtypes => '', prosrc => 'pg_is_standby_requested' },
 
 { oid => '3820', descr => 'current wal flush location',
   proname => 'pg_last_wal_receive_lsn', provolatile => 'v',
-- 
2.34.1


From 9e2be31c38987b565fe2b41b5470659911cf2a10 Mon Sep 17 00:00:00 2001
From: Mikhail Litsarev <m.litsarev@postgrespro.ru>
Date: Tue, 27 Feb 2024 14:08:42 +0300
Subject: [PATCH 2/2] Test pg_is_standby_requested() in different use-cases.

Add tiny tests in src/test/recovery/t/004_timeline_switch.pl
They validate:
 - master is not a replica
 - standby_1 is a replica
 - promoted replica in master mode
 - standby_2 remains a replica after switch to promoted master
---
 src/test/recovery/t/004_timeline_switch.pl | 23 ++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/src/test/recovery/t/004_timeline_switch.pl b/src/test/recovery/t/004_timeline_switch.pl
index 684838cbab7..10a6fa8b905 100644
--- a/src/test/recovery/t/004_timeline_switch.pl
+++ b/src/test/recovery/t/004_timeline_switch.pl
@@ -16,6 +16,11 @@ my $node_primary = PostgreSQL::Test::Cluster->new('primary');
 $node_primary->init(allows_streaming => 1);
 $node_primary->start;
 
+# Validate pg_is_standby_requested() for master
+my $ret_mode_primary = $node_primary->safe_psql('postgres',
+	'SELECT pg_is_standby_requested()');
+is($ret_mode_primary, 'f', "master is not a replica");
+
 # Take backup
 my $backup_name = 'my_backup';
 $node_primary->backup($backup_name);
@@ -30,6 +35,11 @@ $node_standby_2->init_from_backup($node_primary, $backup_name,
 	has_streaming => 1);
 $node_standby_2->start;
 
+# Validate pg_is_standby_requested() for replica node
+my $ret_mode_standby_1 = $node_standby_1->safe_psql('postgres',
+	'SELECT pg_is_standby_requested()');
+is($ret_mode_standby_1, 't', "node_standby_1 is a replica");
+
 # Create some content on primary
 $node_primary->safe_psql('postgres',
 	"CREATE TABLE tab_int AS SELECT generate_series(1,1000) AS a");
@@ -48,6 +58,14 @@ $node_standby_1->psql(
 	stdout => \$psql_out);
 is($psql_out, 't', "promotion of standby with pg_promote");
 
+# Validate pg_is_standby_requested() for master promoted from standby node.
+# pg_is_standby_requested() returns true because standby.signal file
+# was found while being a replica.
+# Use it with pg_is_in_recovery() (returns false), for such use-cases.
+my $ret_mode_1 = $node_standby_1->safe_psql('postgres',
+	'SELECT pg_is_standby_requested()');
+is($ret_mode_1, 't', "node_standby_1 becomes a master");
+
 # Switch standby 2 to replay from standby 1
 my $connstr_1 = $node_standby_1->connstr;
 $node_standby_2->append_conf(
@@ -56,6 +74,11 @@ primary_conninfo='$connstr_1'
 ));
 $node_standby_2->restart;
 
+# Validate pg_is_standby_requested() for second replica after restart
+my $ret_mode_standby_2 = $node_standby_2->safe_psql('postgres',
+	'SELECT pg_is_standby_requested()');
+is($ret_mode_standby_2, 't', "node_standby_2 remains a replica");
+
 # Insert some data in standby 1 and check its presence in standby 2
 # to ensure that the timeline switch has been done.
 $node_standby_1->safe_psql('postgres',
-- 
2.34.1

