public inbox for [email protected]  
help / color / mirror / Atom feed
Synchronizing slots from primary to standby
119+ messages / 18 participants
[nested] [flat]

* Synchronizing slots from primary to standby
@ 2018-12-30 21:23  Petr Jelinek <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Petr Jelinek @ 2018-12-30 21:23 UTC (permalink / raw)
  To: PostgreSQL Hackers <[email protected]>

Hi,

As Andres has mentioned over at minimal decoding on standby thread [1],
that functionality can be used to add simple worker which periodically
synchronizes the slot state from the primary to a standby.

Attached patch is rough implementation of such worker. It's nowhere near
committable in the current state, it servers primarily two purposes - to
have something over what we can agree on the approach (and if we do,
serve as base for that) and to demonstrate that the patch in [1] can
indeed be used for this functionality. All this means that this patch
depends on the [1] to work.

The approach chosen by me is to change the logical replication launcher
to run also on a standby and to support new type of worker which is
started on a standby for the slot synchronization. The new worker
(slotsync) is responsible for periodically fetching information from the
primary server and moving slots on the standby forward (using the fast
forwarding functionality added in PG11) based on that. There is one
worker per database (logical slots are per database, walrcv_exec needs
db connection, etc). I had to add new replication command for listing
slots so that the launcher can check which databases on the upstream
actually have slots and start the slotsync only for those. The second
patch in the series just adds ability to filter which slots are actually
synchronized.

This approach should be eventually portable to logical replication as
well. The only difference there is that we need to be able to map lsns
of the publisher to the lsns of the subscriber. We already do that in
apply so that should be doable, I don't have that as goal for first
version of the feature though.

The basic functionality seems to be working pretty well, however there
are several discussion points and unfinished parts:

a) Do we want to automatically create and drop slots when they get
created on the primary? Currently the patch does auto-create but does
not auto-drop yet. There is no way to signal that slot was dropped so I
don't see straightforward way to differentiate between slots that have
been dropped on master and those that only exist on standby. I guess if
we added the second feature with slot list as well we could drop
anything on that list that's not on primary...
b) The slot creation is somewhat interesting. The slot might be created
while standby does not have wal for existing slots on primary because
they are behind of standby. We solve it by creating ephemeral slot and
wait for the primary slot to pass it's lsn before persisting it
(similarly to when we are trying to build initial snapshot). This seems
reasonable to me but the coding could use another pair of eyes there.
c) With the periodical start/stop (for the move) of the decoding on the
slot, the logging of every start of decoding context is pretty
annoying/spammy, we should probably tune that down.
d) The launcher integration needs improvement - add worker kind rather
than guessing from values of dbid, subid and relid and do decisions
based on that. Also the interfaces for manipulating the workers should
probably use LogicalRepWorkerId rather than above mentioned parameters
and guessing everywhere.
e) We probably should support synchronizing physical slots as well
(currently we only sync logical slots). But that should be easy provided
we don't mind that logical replication launcher is somewhat misnomer then...
f) Maybe walreceiver or startup should signal these new workers if
enough data is processed, so it's not purely time based. But I think
that kind of optimization can be left for later.

Also (these are pretty pointless until we agree that this is the right
approach):
- there is no documentation update yet
- there are no TAP tests yet
- the recheck timer might need GUC

[1]
https://www.postgresql.org/message-id/[email protected]

-- 
  Petr Jelinek                  http://www.2ndQuadrant.com/
  PostgreSQL Development, 24x7 Support, Training & Services


Attachments:

  [text/x-patch] 0002-Add-option-for-filtering-which-slots-get-synchronize.patch (13.7K, ../../[email protected]/2-0002-Add-option-for-filtering-which-slots-get-synchronize.patch)
  download | inline diff:
From 719ac03dd963502381fbc210947faae472b4a95a Mon Sep 17 00:00:00 2001
From: Petr Jelinek <[email protected]>
Date: Sun, 30 Dec 2018 02:07:26 +0100
Subject: [PATCH 2/2] Add option for filtering which slots get synchronized

---
 .../libpqwalreceiver/libpqwalreceiver.c       |  32 +++++-
 src/backend/replication/logical/launcher.c    |   3 +-
 src/backend/replication/logical/slotsync.c    | 104 +++++++++++++++++-
 src/backend/replication/repl_gram.y           |  23 +++-
 src/backend/replication/walsender.c           |  37 ++++++-
 src/backend/utils/misc/guc.c                  |  12 ++
 src/include/nodes/replnodes.h                 |   1 +
 src/include/replication/logicalworker.h       |   9 ++
 src/include/replication/walreceiver.h         |   6 +-
 src/include/replication/worker_internal.h     |   4 +
 10 files changed, 216 insertions(+), 15 deletions(-)

diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 3c77a76ea6..f564f288d0 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -59,7 +59,8 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 						 TimeLineID *primary_tli,
 						 int *server_version);
-static List *libpqrcv_list_slots(WalReceiverConn *conn);
+static List *libpqrcv_list_slots(WalReceiverConn *conn, int nslot_names,
+					NameData *slot_names);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 								 TimeLineID tli, char **filename,
 								 char **content, int *len);
@@ -355,15 +356,34 @@ libpqrcv_identify_system(WalReceiverConn *conn, TimeLineID *primary_tli,
  * Get list of slots from primary.
  */
 static List *
-libpqrcv_list_slots(WalReceiverConn *conn)
+libpqrcv_list_slots(WalReceiverConn *conn, int nslot_names,
+					NameData *slot_names)
 {
 	PGresult   *res;
 	int			i;
-	List	   *slots = NIL;
+	List	   *slotlist = NIL;
 	int			ntuples;
+	StringInfoData	s;
 	WalRecvReplicationSlotData *slot_data;
 
-	res = libpqrcv_PQexec(conn->streamConn, "LIST_SLOTS");
+	initStringInfo(&s);
+	appendStringInfoString(&s, "LIST_SLOTS");
+	if (nslot_names > 0)
+	{
+		int				i;
+
+		appendStringInfoChar(&s, ' ');
+		for (i = 0; i < nslot_names; i++)
+		{
+			if (i > 0)
+				appendStringInfoChar(&s, ',');
+			appendStringInfo(&s, "%s",
+							 quote_identifier(NameStr(slot_names[i])));
+		}
+	}
+
+	res = libpqrcv_PQexec(conn->streamConn, s.data);
+	pfree(s.data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
 		PQclear(res);
@@ -414,12 +434,12 @@ libpqrcv_list_slots(WalReceiverConn *conn)
 			slot_data->confirmed_flush = pg_strtouint64(PQgetvalue(res, i, 9),
 														NULL, 10);
 
-		slots = lappend(slots, slot_data);
+		slotlist = lappend(slotlist, slot_data);
 	}
 
 	PQclear(res);
 
-	return slots;
+	return slotlist;
 }
 
 /*
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index c47d4ee403..161205d9e4 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -1005,7 +1005,8 @@ ApplyLauncherStartSlotSync(TimestampTz *last_start_time, long *wait_time)
 									ALLOCSET_DEFAULT_SIZES);
 	oldctx = MemoryContextSwitchTo(tmpctx);
 
-	slots = walrcv_list_slots(wrconn);
+	slots = walrcv_list_slots(wrconn, numsynchronize_slot_names,
+							  synchronize_slot_names);
 
 	now = GetCurrentTimestamp();
 
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index eda9ad0add..07f06a8fe9 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -26,7 +26,11 @@
 #include "utils/builtins.h"
 #include "utils/guc.h"
 #include "utils/pg_lsn.h"
+#include "utils/varlena.h"
 
+char	*synchronize_slot_names_string;
+NameData *synchronize_slot_names;
+int numsynchronize_slot_names;
 
 /*
  * Wait for remote slot to pass localy reserved position.
@@ -215,12 +219,26 @@ synchronize_slots(void)
 				(errmsg("could not connect to the primary server: %s", err)));
 
 	resetStringInfo(&s);
-	/* TODO filter slot names? */
 	appendStringInfo(&s,
 					 "SELECT slot_name, plugin, confirmed_flush_lsn"
 					 "  FROM pg_catalog.pg_replication_slots"
 					 " WHERE database = %s",
 					 quote_literal_cstr(database));
+	if (numsynchronize_slot_names > 0)
+	{
+		int				i;
+
+		appendStringInfoString(&s, " AND slot_name IN (");
+		for (i = 0; i < numsynchronize_slot_names; i++)
+		{
+			if (i > 0)
+				appendStringInfoChar(&s, ',');
+			appendStringInfo(&s, "%s",
+					quote_literal_cstr(NameStr(synchronize_slot_names[i])));
+		}
+		appendStringInfoChar(&s, ')');
+	}
+
 	res = walrcv_exec(wrconn, s.data, 3, slotRow);
 	pfree(s.data);
 
@@ -300,6 +318,9 @@ ReplSlotSyncMain(Datum main_arg)
 		if (!RecoveryInProgress())
 			return;
 
+		if (numsynchronize_slot_names == 0)
+			return;
+
 		synchronize_slots();
 
 		rc = WaitLatch(MyLatch,
@@ -314,3 +335,84 @@ ReplSlotSyncMain(Datum main_arg)
 			proc_exit(1);
 	}
 }
+
+/*
+ * Routines for handling the GUC variable(s)
+ */
+
+typedef struct
+{
+	int			numslots;
+	NameData	slots[FLEXIBLE_ARRAY_MEMBER];
+} synchronize_slot_names_extra;
+
+bool
+check_synchronize_slot_names(char **newval, void **extra, GucSource source)
+{
+	char	   *rawname;
+	List	   *namelist;
+	ListCell   *lc;
+	synchronize_slot_names_extra *myextra;
+	NameData   *slots;
+	int			numslots;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Parse string into list of identifiers */
+	if (!SplitIdentifierString(rawname, ',', &namelist))
+	{
+		/* syntax error in name list */
+		GUC_check_errdetail("List syntax is invalid.");
+		pfree(rawname);
+		list_free(namelist);
+		return false;
+	}
+
+	/* temporary workspace until we are done verifying the list */
+	slots = (NameData *) palloc(list_length(namelist) * sizeof(NameData));
+	numslots = 0;
+	foreach(lc, namelist)
+	{
+		char	   *curname = (char *) lfirst(lc);
+
+		/* Special handling for "*" which means all. */
+		if (strcmp(curname, "*") == 0)
+		{
+			numslots = -1;
+			break;
+		}
+
+		/* For any other value, validate slot name. */
+		ReplicationSlotValidateName(curname, ERROR);
+
+		/* And add it to our array. */
+		namestrcpy(&slots[numslots++], curname);
+	}
+
+	/* Now prepare an "extra" struct for assign_temp_tablespaces */
+	myextra = malloc(offsetof(synchronize_slot_names_extra, slots) +
+					 numslots > 0 ? numslots * sizeof(NameData) : 0);
+	if (!myextra)
+		return false;
+	myextra->numslots = numslots;
+	if (numslots > 0)
+		memcpy(myextra->slots, slots, numslots * sizeof(NameData));
+	*extra = (void *) myextra;
+
+	pfree(slots);
+	pfree(rawname);
+	list_free(namelist);
+
+	return true;
+}
+
+void
+assign_synchronize_slot_names(const char *newval, void *extra)
+{
+	synchronize_slot_names_extra *myextra =
+		(synchronize_slot_names_extra *) extra;
+
+	synchronize_slot_names = myextra->slots;
+	numsynchronize_slot_names = myextra->numslots;
+}
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95d91bf236..f0988aed1c 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -103,6 +103,7 @@ static SQLCmd *make_sqlcmd(void);
 %type <boolval>	opt_temporary
 %type <list>	create_slot_opt_list
 %type <defelt>	create_slot_opt
+%type <list>	slot_name_list slot_name_list_opt
 
 %%
 
@@ -138,13 +139,31 @@ identify_system:
 					$$ = (Node *) makeNode(IdentifySystemCmd);
 				}
 			;
+
+slot_name_list:
+			IDENT
+				{
+					$$ = list_make1($1);
+				}
+			| slot_name_list ',' IDENT
+				{
+					$$ = lappend($1, $3);
+				}
+
+slot_name_list_opt:
+			slot_name_list			{ $$ = $1; }
+			| /* EMPTY */			{ $$ = NIL; }
+		;
+
 /*
  * LIST_SLOTS
  */
 list_slots:
-			K_LIST_SLOTS
+			K_LIST_SLOTS slot_name_list_opt
 				{
-					$$ = (Node *) makeNode(ListSlotsCmd);
+					ListSlotsCmd *cmd = makeNode(ListSlotsCmd);
+					cmd->slot_names = $2;
+					$$ = (Node *) cmd;
 				}
 			;
 
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 6ca304b8f5..bb9cec46a8 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -426,16 +426,41 @@ IdentifySystem(void)
 	end_tup_output(tstate);
 }
 
+static int
+pg_qsort_namecmp(const void *a, const void *b)
+{
+	return strncmp(NameStr(*(Name)a), NameStr(*(Name)b), NAMEDATALEN);
+}
 /*
  * Handle the LIST_SLOTS command.
  */
 static void
-ListSlots(void)
+ListSlots(ListSlotsCmd *cmd)
 {
 	DestReceiver *dest;
 	TupOutputState *tstate;
 	TupleDesc	tupdesc;
 	int			slotno;
+	NameData   *slot_names;
+	int			numslot_names;
+
+	numslot_names = list_length(cmd->slot_names);
+	if (numslot_names)
+	{
+		ListCell *lc;
+		int		  i = 0;
+
+		slot_names = palloc(numslot_names * sizeof(NameData));
+		foreach (lc, cmd->slot_names)
+		{
+			char *slot_name = lfirst(lc);
+
+			ReplicationSlotValidateName(slot_name, ERROR);
+			namestrcpy(&slot_names[i++], slot_name);
+		}
+
+		qsort(slot_names, numslot_names, sizeof(NameData), pg_qsort_namecmp);
+	}
 
 	dest = CreateDestReceiver(DestRemoteSimple);
 
@@ -501,6 +526,11 @@ ListSlots(void)
 
 		SpinLockRelease(&slot->mutex);
 
+		if (numslot_names &&
+			!bsearch((void *) &slot_name, (void *) slot_names,
+					 numslot_names, sizeof(NameData), pg_qsort_namecmp))
+			continue;
+
 		memset(nulls, 0, sizeof(nulls));
 
 		i = 0;
@@ -1712,7 +1742,10 @@ exec_replication_command(const char *cmd_string)
 			break;
 
 		case T_ListSlotsCmd:
-			ListSlots();
+			{
+				ListSlotsCmd *cmd = (ListSlotsCmd *) cmd_node;
+				ListSlots(cmd);
+			}
 			break;
 
 		case T_SQLCmd:
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 6fe1939881..338232920b 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -64,6 +64,7 @@
 #include "postmaster/syslogger.h"
 #include "postmaster/walwriter.h"
 #include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walreceiver.h"
@@ -4078,6 +4079,17 @@ static struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"synchronize_slot_names", PGC_SIGHUP, REPLICATION_STANDBY,
+			gettext_noop("Sets the names of replication slots which to synchronize from primary to standby."),
+			gettext_noop("Value of \"*\" means all."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&synchronize_slot_names_string,
+		"",
+		check_synchronize_slot_names, assign_synchronize_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index 1101f71bcc..71f8f8ca8e 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -40,6 +40,7 @@ typedef struct IdentifySystemCmd
 typedef struct ListSlotsCmd
 {
 	NodeTag		type;
+	List	   *slot_names;
 } ListSlotsCmd;
 
 /* ----------------------
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index 6379aae7ce..1b09a1fb3e 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -12,8 +12,17 @@
 #ifndef LOGICALWORKER_H
 #define LOGICALWORKER_H
 
+#include "utils/guc.h"
+
+extern char *synchronize_slot_names_string;
+
 extern void ApplyWorkerMain(Datum main_arg);
+extern void ReplSlotSyncMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 
+extern bool check_synchronize_slot_names(char **newval, void **extra, GucSource source);
+extern void assign_synchronize_slot_names(const char *newval, void *extra);
+
+
 #endif							/* LOGICALWORKER_H */
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 9014f92bfa..2593af2291 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -219,7 +219,7 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli,
 											int *server_version);
-typedef List *(*walrcv_list_slots_fn) (WalReceiverConn *conn);
+typedef List *(*walrcv_list_slots_fn) (WalReceiverConn *conn, int nslots, NameData *slots);
 typedef void (*walrcv_readtimelinehistoryfile_fn) (WalReceiverConn *conn,
 												   TimeLineID tli,
 												   char **filename,
@@ -272,8 +272,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli, server_version) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli, server_version)
-#define walrcv_list_slots(conn) \
-	WalReceiverFunctions->walrcv_list_slots(conn)
+#define walrcv_list_slots(conn, nslots, slots) \
+	WalReceiverFunctions->walrcv_list_slots(conn, nslots, slots)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
 	WalReceiverFunctions->walrcv_readtimelinehistoryfile(conn, tli, filename, content, size)
 #define walrcv_startstreaming(conn, options) \
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index ba2d3f1fca..c2373fea47 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -68,6 +68,10 @@ extern LogicalRepWorker *MyLogicalRepWorker;
 
 extern bool in_remote_transaction;
 
+/* Translated Gucs we share with launcher and worker. */
+extern NameData *synchronize_slot_names;
+extern int numsynchronize_slot_names;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid dbid, Oid subid, Oid relid,
 					   bool only_running);
-- 
2.17.1



  [text/x-patch] 0001-Synchronize-logical-replication-slots-from-primary-t.patch (39.6K, ../../[email protected]/3-0001-Synchronize-logical-replication-slots-from-primary-t.patch)
  download | inline diff:
From 87fa57753cb667ebcabee48b3fead835de72d6e1 Mon Sep 17 00:00:00 2001
From: Petr Jelinek <[email protected]>
Date: Sun, 30 Dec 2018 02:07:12 +0100
Subject: [PATCH 1/2] Synchronize logical replication slots from primary to
 standby

---
 src/backend/commands/subscriptioncmds.c       |   4 +-
 src/backend/postmaster/bgworker.c             |   3 +
 src/backend/postmaster/pgstat.c               |   3 +
 .../libpqwalreceiver/libpqwalreceiver.c       |  74 ++++
 src/backend/replication/logical/Makefile      |   3 +-
 src/backend/replication/logical/launcher.c    | 207 ++++++++----
 src/backend/replication/logical/slotsync.c    | 316 ++++++++++++++++++
 src/backend/replication/logical/tablesync.c   |  12 +-
 src/backend/replication/repl_gram.y           |  13 +-
 src/backend/replication/repl_scanner.l        |   1 +
 src/backend/replication/slotfuncs.c           |   2 +-
 src/backend/replication/walsender.c           | 164 +++++++++
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/replnodes.h                 |   8 +
 src/include/pgstat.h                          |   1 +
 src/include/replication/slot.h                |   3 +
 src/include/replication/walreceiver.h         |  12 +
 src/include/replication/worker_internal.h     |   8 +-
 18 files changed, 764 insertions(+), 71 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 9021463a4c..bec6d11457 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -598,7 +598,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
 		{
 			RemoveSubscriptionRel(sub->oid, relid);
 
-			logicalrep_worker_stop_at_commit(sub->oid, relid);
+			logicalrep_worker_stop_at_commit(MyDatabaseId, sub->oid, relid);
 
 			ereport(DEBUG1,
 					(errmsg("table \"%s.%s\" removed from subscription \"%s\"",
@@ -940,7 +940,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	{
 		LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc);
 
-		logicalrep_worker_stop(w->subid, w->relid);
+		logicalrep_worker_stop(w->dbid, w->subid, w->relid);
 	}
 	list_free(subworkers);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index d2b695e146..3b2a5d04cd 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -129,6 +129,9 @@ static const struct
 	},
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
+	},
+	{
+		"ReplSlotSyncMain", ReplSlotSyncMain
 	}
 };
 
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 8676088e57..5387f4f2fa 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -3494,6 +3494,9 @@ pgstat_get_wait_activity(WaitEventActivity w)
 		case WAIT_EVENT_LOGICAL_LAUNCHER_MAIN:
 			event_name = "LogicalLauncherMain";
 			break;
+		case WAIT_EVENT_REPL_SLOT_SYNC_MAIN:
+			event_name = "ReplSlotSyncMain";
+			break;
 		case WAIT_EVENT_PGSTAT_MAIN:
 			event_name = "PgStatMain";
 			break;
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 9b75711ebd..3c77a76ea6 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -23,6 +23,7 @@
 #include "pqexpbuffer.h"
 #include "access/xlog.h"
 #include "catalog/pg_type.h"
+#include "commands/dbcommands.h"
 #include "funcapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 						 TimeLineID *primary_tli,
 						 int *server_version);
+static List *libpqrcv_list_slots(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 								 TimeLineID tli, char **filename,
 								 char **content, int *len);
@@ -86,6 +88,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	libpqrcv_get_conninfo,
 	libpqrcv_get_senderinfo,
 	libpqrcv_identify_system,
+	libpqrcv_list_slots,
 	libpqrcv_readtimelinehistoryfile,
 	libpqrcv_startstreaming,
 	libpqrcv_endstreaming,
@@ -348,6 +351,77 @@ libpqrcv_identify_system(WalReceiverConn *conn, TimeLineID *primary_tli,
 	return primary_sysid;
 }
 
+/*
+ * Get list of slots from primary.
+ */
+static List *
+libpqrcv_list_slots(WalReceiverConn *conn)
+{
+	PGresult   *res;
+	int			i;
+	List	   *slots = NIL;
+	int			ntuples;
+	WalRecvReplicationSlotData *slot_data;
+
+	res = libpqrcv_PQexec(conn->streamConn, "LIST_SLOTS");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("could not receive list of slots the primary server: %s",
+						pchomp(PQerrorMessage(conn->streamConn)))));
+	}
+	if (PQnfields(res) < 10)
+	{
+		int			nfields = PQnfields(res);
+
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("invalid response from primary server"),
+				 errdetail("Could not get list of slots: got %d fields, expected %d or more fields.",
+						   nfields, 10)));
+	}
+
+	ntuples = PQntuples(res);
+	for (i = 0; i < ntuples; i++)
+	{
+		char   *slot_type;
+
+		slot_data = palloc0(sizeof(WalRecvReplicationSlotData));
+		namestrcpy(&slot_data->name, PQgetvalue(res, i, 0));
+		if (!PQgetisnull(res, i, 1))
+			namestrcpy(&slot_data->plugin, PQgetvalue(res, i, 1));
+		slot_type = PQgetvalue(res, i, 2);
+		if (!PQgetisnull(res, i, 3))
+			slot_data->database = atooid(PQgetvalue(res, i, 3));
+		if (strcmp(slot_type, "physical") == 0)
+		{
+			if (OidIsValid(slot_data->database))
+				elog(ERROR, "unexpected physical replication slot with database set");
+		}
+		if (pg_strtoint32(PQgetvalue(res, i, 5)) == 1)
+			slot_data->persistency = RS_TEMPORARY;
+		else
+			slot_data->persistency = RS_PERSISTENT;
+		if (!PQgetisnull(res, i, 6))
+			slot_data->xmin = atooid(PQgetvalue(res, i, 6));
+		if (!PQgetisnull(res, i, 7))
+			slot_data->catalog_xmin = atooid(PQgetvalue(res, i, 7));
+		if (!PQgetisnull(res, i, 8))
+			slot_data->restart_lsn = pg_strtouint64(PQgetvalue(res, i, 8),
+													NULL, 10);
+		if (!PQgetisnull(res, i, 9))
+			slot_data->confirmed_flush = pg_strtouint64(PQgetvalue(res, i, 9),
+														NULL, 10);
+
+		slots = lappend(slots, slot_data);
+	}
+
+	PQclear(res);
+
+	return slots;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index bb417b042e..385782bd67 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 override CPPFLAGS := -I$(srcdir) $(CPPFLAGS)
 
 OBJS = decode.o launcher.o logical.o logicalfuncs.o message.o origin.o \
-	   proto.o relation.o reorderbuffer.o snapbuild.o tablesync.o worker.o
+	   proto.o relation.o reorderbuffer.o slotsync.o snapbuild.o \
+	   tablesync.o worker.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 3a84d8ca86..c47d4ee403 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -26,6 +26,7 @@
 #include "access/htup_details.h"
 #include "access/xact.h"
 
+#include "catalog/pg_authid.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 
@@ -75,6 +76,7 @@ LogicalRepCtxStruct *LogicalRepCtx;
 
 typedef struct LogicalRepWorkerId
 {
+	Oid			dbid;
 	Oid			subid;
 	Oid			relid;
 } LogicalRepWorkerId;
@@ -239,7 +241,7 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker,
  * subscription id and relid.
  */
 LogicalRepWorker *
-logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
+logicalrep_worker_find(Oid dbid, Oid subid, Oid relid, bool only_running)
 {
 	int			i;
 	LogicalRepWorker *res = NULL;
@@ -251,8 +253,8 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
-		if (w->in_use && w->subid == subid && w->relid == relid &&
-			(!only_running || w->proc))
+		if (w->in_use && w->dbid == dbid && w->subid == subid &&
+			w->relid == relid && (!only_running || w->proc))
 		{
 			res = w;
 			break;
@@ -302,9 +304,13 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	int			nsyncworkers;
 	TimestampTz now;
 
-	ereport(DEBUG1,
-			(errmsg("starting logical replication worker for subscription \"%s\"",
-					subname)));
+	if (OidIsValid(subid))
+		ereport(DEBUG1,
+				(errmsg("starting logical replication worker for subscription \"%s\"",
+						subname)));
+	else
+		ereport(DEBUG1,
+				(errmsg("starting replication slot synchronization worker")));
 
 	/* Report this after the initial starting message for consistency. */
 	if (max_replication_slots == 0)
@@ -341,7 +347,9 @@ retry:
 	 * reason we do this is because if some worker failed to start up and its
 	 * parent has crashed while waiting, the in_use state was never cleared.
 	 */
-	if (worker == NULL || nsyncworkers >= max_sync_workers_per_subscription)
+	if (worker == NULL ||
+		(OidIsValid(relid) &&
+		 nsyncworkers >= max_sync_workers_per_subscription))
 	{
 		bool		did_cleanup = false;
 
@@ -375,7 +383,7 @@ retry:
 	 * silently as we might get here because of an otherwise harmless race
 	 * condition.
 	 */
-	if (nsyncworkers >= max_sync_workers_per_subscription)
+	if (OidIsValid(relid) && nsyncworkers >= max_sync_workers_per_subscription)
 	{
 		LWLockRelease(LogicalRepWorkerLock);
 		return;
@@ -421,15 +429,22 @@ retry:
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	if (OidIsValid(subid))
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	else
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncMain");
 	if (OidIsValid(relid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u sync %u", subid, relid);
-	else
+	else if (OidIsValid(subid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u", subid);
+	else
+		snprintf(bgw.bgw_name, BGW_MAXLEN,
+				 "replication slot synchronization worker");
+
 	snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication worker");
 
 	bgw.bgw_restart_time = BGW_NEVER_RESTART;
@@ -460,14 +475,14 @@ retry:
  * it detaches from the slot.
  */
 void
-logicalrep_worker_stop(Oid subid, Oid relid)
+logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 	uint16		generation;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, false);
+	worker = logicalrep_worker_find(dbid, subid, relid, false);
 
 	/* No worker, nothing to do. */
 	if (!worker)
@@ -557,7 +572,7 @@ logicalrep_worker_stop(Oid subid, Oid relid)
  * Request worker for specified sub/rel to be stopped on commit.
  */
 void
-logicalrep_worker_stop_at_commit(Oid subid, Oid relid)
+logicalrep_worker_stop_at_commit(Oid dbid, Oid subid, Oid relid)
 {
 	int			nestDepth = GetCurrentTransactionNestLevel();
 	LogicalRepWorkerId *wid;
@@ -590,6 +605,7 @@ logicalrep_worker_stop_at_commit(Oid subid, Oid relid)
 	 * subtransaction.
 	 */
 	wid = palloc(sizeof(LogicalRepWorkerId));
+	wid->dbid = dbid;
 	wid->subid = subid;
 	wid->relid = relid;
 	on_commit_stop_workers->workers =
@@ -602,13 +618,13 @@ logicalrep_worker_stop_at_commit(Oid subid, Oid relid)
  * Wake up (using latch) any logical replication worker for specified sub/rel.
  */
 void
-logicalrep_worker_wakeup(Oid subid, Oid relid)
+logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, true);
+	worker = logicalrep_worker_find(dbid, subid, relid, true);
 
 	if (worker)
 		logicalrep_worker_wakeup_ptr(worker);
@@ -795,7 +811,7 @@ ApplyLauncherRegister(void)
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
 	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyLauncherMain");
 	snprintf(bgw.bgw_name, BGW_MAXLEN,
@@ -873,7 +889,7 @@ AtEOXact_ApplyLauncher(bool isCommit)
 			{
 				LogicalRepWorkerId *wid = lfirst(lc);
 
-				logicalrep_worker_stop(wid->subid, wid->relid);
+				logicalrep_worker_stop(wid->dbid, wid->subid, wid->relid);
 			}
 		}
 
@@ -964,6 +980,115 @@ ApplyLauncherWakeup(void)
 		kill(LogicalRepCtx->launcher_pid, SIGUSR1);
 }
 
+static void
+ApplyLauncherStartSlotSync(TimestampTz *last_start_time, long *wait_time)
+{
+	TimestampTz now;
+	char	   *err;
+	List	   *slots;
+	ListCell   *lc;
+	MemoryContext tmpctx;
+	MemoryContext oldctx;
+
+	if (numsynchronize_slot_names == 0)
+		return;
+
+	wrconn = walrcv_connect(PrimaryConnInfo, false,
+							"Logical Replication Launcher", &err);
+	if (!wrconn)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	/* Use temporary context for the slot list and worker info. */
+	tmpctx = AllocSetContextCreate(TopMemoryContext,
+									"Logical Replication Launcher slot sync ctx",
+									ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(tmpctx);
+
+	slots = walrcv_list_slots(wrconn);
+
+	now = GetCurrentTimestamp();
+
+	foreach (lc, slots)
+	{
+		WalRecvReplicationSlotData *slot_data = lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!OidIsValid(slot_data->database))
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(slot_data->database, InvalidOid,
+								   InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(slot_data->database, InvalidOid, NULL,
+									 BOOTSTRAP_SUPERUSERID, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(tmpctx);
+
+	walrcv_disconnect(wrconn);
+}
+
+static void
+ApplyLauncherStartSubs(TimestampTz *last_start_time, long *wait_time)
+{
+	TimestampTz now;
+	List	   *sublist;
+	ListCell   *lc;
+	MemoryContext subctx;
+	MemoryContext oldctx;
+
+	now = GetCurrentTimestamp();
+
+	/* Use temporary context for the database list and worker info. */
+	subctx = AllocSetContextCreate(TopMemoryContext,
+								   "Logical Replication Launcher sublist",
+								   ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(subctx);
+
+	/* search for subscriptions to start or stop. */
+	sublist = get_subscription_list();
+
+	/* Start the missing workers for enabled subscriptions. */
+	foreach(lc, sublist)
+	{
+		Subscription *sub = (Subscription *) lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!sub->enabled)
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(sub->dbid, sub->oid, InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
+									 sub->owner, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(subctx);
+}
+
 /*
  * Main loop for the apply launcher process.
  */
@@ -991,14 +1116,12 @@ ApplyLauncherMain(Datum main_arg)
 	 */
 	BackgroundWorkerInitializeConnection(NULL, NULL, 0);
 
+	load_file("libpqwalreceiver", false);
+
 	/* Enter main loop */
 	for (;;)
 	{
 		int			rc;
-		List	   *sublist;
-		ListCell   *lc;
-		MemoryContext subctx;
-		MemoryContext oldctx;
 		TimestampTz now;
 		long		wait_time = DEFAULT_NAPTIME_PER_CYCLE;
 
@@ -1010,42 +1133,10 @@ ApplyLauncherMain(Datum main_arg)
 		if (TimestampDifferenceExceeds(last_start_time, now,
 									   wal_retrieve_retry_interval))
 		{
-			/* Use temporary context for the database list and worker info. */
-			subctx = AllocSetContextCreate(TopMemoryContext,
-										   "Logical Replication Launcher sublist",
-										   ALLOCSET_DEFAULT_SIZES);
-			oldctx = MemoryContextSwitchTo(subctx);
-
-			/* search for subscriptions to start or stop. */
-			sublist = get_subscription_list();
-
-			/* Start the missing workers for enabled subscriptions. */
-			foreach(lc, sublist)
-			{
-				Subscription *sub = (Subscription *) lfirst(lc);
-				LogicalRepWorker *w;
-
-				if (!sub->enabled)
-					continue;
-
-				LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-				w = logicalrep_worker_find(sub->oid, InvalidOid, false);
-				LWLockRelease(LogicalRepWorkerLock);
-
-				if (w == NULL)
-				{
-					last_start_time = now;
-					wait_time = wal_retrieve_retry_interval;
-
-					logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
-											 sub->owner, InvalidOid);
-				}
-			}
-
-			/* Switch back to original memory context. */
-			MemoryContextSwitchTo(oldctx);
-			/* Clean the temporary memory. */
-			MemoryContextDelete(subctx);
+			if (!RecoveryInProgress())
+				ApplyLauncherStartSubs(&last_start_time, &wait_time);
+			else
+				ApplyLauncherStartSlotSync(&last_start_time, &wait_time);
 		}
 		else
 		{
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..eda9ad0add
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,316 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby from primary
+ *
+ * Copyright (c) 2016-2018, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "catalog/pg_type_d.h"
+#include "commands/dbcommands.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "replication/logicalworker.h"
+#include "replication/slot.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/pg_lsn.h"
+
+
+/*
+ * Wait for remote slot to pass localy reserved position.
+ */
+static void
+wait_for_primary_slot_catchup(WalReceiverConn *wrconn, char *slot_name,
+							  XLogRecPtr min_lsn)
+{
+	WalRcvExecResult *res;
+	TupleTableSlot *slot;
+	Oid				slotRow[1] = {LSNOID};
+	StringInfoData	cmd;
+	bool			isnull;
+	XLogRecPtr		restart_lsn;
+
+	for (;;)
+	{
+		int	rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		initStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "SELECT restart_lsn"
+						 "  FROM pg_catalog.pg_replication_slots"
+						 " WHERE slot_name = %s",
+						 quote_literal_cstr(slot_name));
+		res = walrcv_exec(wrconn, cmd.data, 1, slotRow);
+
+		if (res->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errmsg("could not fetch slot info for slot \"%s\" from primary: %s",
+							slot_name, res->err)));
+
+		slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+		if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+			ereport(ERROR,
+					(errmsg("slot \"%s\" disapeared from provider",
+							slot_name)));
+
+		restart_lsn = DatumGetLSN(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		ExecClearTuple(slot);
+		walrcv_clear_result(res);
+
+		if (restart_lsn >= min_lsn)
+			break;
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This optionally creates new slot if there is no existing one.
+ */
+static void
+synchronize_one_slot(WalReceiverConn *wrconn, char *slot_name, char *database,
+					 char *plugin_name, XLogRecPtr target_lsn)
+{
+	int			i;
+	bool		found = false;
+	XLogRecPtr	endlsn;
+
+	/* Search for the named slot and mark it active if we find it. */
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (!s->in_use)
+			continue;
+
+		if (strcmp(NameStr(s->data.name), slot_name) == 0)
+		{
+			found = true;
+			break;
+		}
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	StartTransactionCommand();
+
+	/* Already existing slot, acquire */
+	if (found)
+	{
+		ReplicationSlotAcquire(slot_name, true);
+
+		if (target_lsn < MyReplicationSlot->data.confirmed_flush)
+		{
+			elog(DEBUG1,
+				 "not synchronizing slot %s; synchronization would move it backward",
+				 slot_name);
+
+			ReplicationSlotRelease();
+			CommitTransactionCommand();
+			return;
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+		ReplicationSlot	   *slot;
+
+		ReplicationSlotCreate(slot_name, true, RS_EPHEMERAL);
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(database, false);
+		StrNCpy(NameStr(slot->data.plugin), plugin_name, NAMEDATALEN);
+		SpinLockRelease(&slot->mutex);
+
+		ReplicationSlotReserveWal();
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		if (target_lsn < MyReplicationSlot->data.restart_lsn)
+		{
+			elog(LOG, "waiting for remote slot %s lsn (%X/%X) to pass local slot lsn (%X/%X)",
+				 slot_name,
+				 (uint32) (target_lsn >> 32),
+				 (uint32) (target_lsn),
+				 (uint32) (MyReplicationSlot->data.restart_lsn >> 32),
+				 (uint32) (MyReplicationSlot->data.restart_lsn));
+
+			wait_for_primary_slot_catchup(wrconn, slot_name,
+										  MyReplicationSlot->data.restart_lsn);
+		}
+
+		ReplicationSlotPersist();
+	}
+
+	endlsn = pg_logical_replication_slot_advance(target_lsn);
+
+	elog(DEBUG3, "synchronized slot %s to lsn (%X/%X)",
+		 slot_name, (uint32) (endlsn >> 32), (uint32) (endlsn));
+
+	ReplicationSlotRelease();
+	CommitTransactionCommand();
+}
+
+static void
+synchronize_slots(void)
+{
+	WalRcvExecResult *res;
+	WalReceiverConn *wrconn = NULL;
+	TupleTableSlot *slot;
+	Oid				slotRow[3] = {TEXTOID, TEXTOID, LSNOID};
+	StringInfoData	s;
+	char		   *database;
+	char		   *err;
+	MemoryContext	oldctx = CurrentMemoryContext;
+
+	if (!WalRcv)
+		return;
+
+	/* syscache access needs a transaction env. */
+	StartTransactionCommand();
+	/* make dbname live outside TX context */
+	MemoryContextSwitchTo(oldctx);
+
+	database = get_database_name(MyDatabaseId);
+	initStringInfo(&s);
+	appendStringInfo(&s, "%s dbname=%s", PrimaryConnInfo, database);
+	wrconn = walrcv_connect(s.data, true, "slot_sync", &err);
+
+	if (wrconn == NULL)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	resetStringInfo(&s);
+	/* TODO filter slot names? */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn"
+					 "  FROM pg_catalog.pg_replication_slots"
+					 " WHERE database = %s",
+					 quote_literal_cstr(database));
+	res = walrcv_exec(wrconn, s.data, 3, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				(errmsg("could not fetch slot info from primary: %s",
+						res->err)));
+
+	CommitTransactionCommand();
+	/* CommitTransactionCommand switches to TopMemoryContext */
+	MemoryContextSwitchTo(oldctx);
+
+	slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+	{
+		char	   *slot_name;
+		char	   *plugin_name;
+		XLogRecPtr	confirmed_flush_lsn;
+		bool		isnull;
+
+		slot_name = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		plugin_name = TextDatumGetCString(slot_getattr(slot, 2, &isnull));
+		Assert(!isnull);
+
+		confirmed_flush_lsn = DatumGetLSN(slot_getattr(slot, 3, &isnull));
+		Assert(!isnull);
+
+		synchronize_one_slot(wrconn, slot_name, database, plugin_name,
+							 confirmed_flush_lsn);
+
+		ExecClearTuple(slot);
+	}
+
+	walrcv_clear_result(res);
+	pfree(database);
+
+	walrcv_disconnect(wrconn);
+}
+
+/*
+ * The main loop of our worker process.
+ */
+void
+ReplSlotSyncMain(Datum main_arg)
+{
+	int			worker_slot = DatumGetInt32(main_arg);
+
+	/* Attach to slot */
+	logicalrep_worker_attach(worker_slot);
+
+	/* Establish signal handlers. */
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	/* Connect to our database. */
+	BackgroundWorkerInitializeConnectionByOid(MyLogicalRepWorker->dbid,
+											  MyLogicalRepWorker->userid,
+											  0);
+
+	StartTransactionCommand();
+	ereport(LOG,
+			(errmsg("replication slot synchronization worker for database \"%s\" has started",
+					get_database_name(MyLogicalRepWorker->dbid))));
+	CommitTransactionCommand();
+
+	/* Main wait loop. */
+	for (;;)
+	{
+		int		rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		if (!RecoveryInProgress())
+			return;
+
+		synchronize_slots();
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 38ae1b9ab8..816468a782 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -141,7 +141,8 @@ finish_sync_worker(void)
 	CommitTransactionCommand();
 
 	/* Find the main apply worker and signal it. */
-	logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid);
+	logicalrep_worker_wakeup(MyLogicalRepWorker->dbid,
+							 MyLogicalRepWorker->subid, InvalidOid);
 
 	/* Stop gracefully */
 	proc_exit(0);
@@ -184,7 +185,8 @@ wait_for_relation_state_change(Oid relid, char expected_state)
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
 		/* Check if the opposite worker is still running and bail if not. */
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid,
 										am_tablesync_worker() ? InvalidOid : relid,
 										false);
 		LWLockRelease(LogicalRepWorkerLock);
@@ -232,7 +234,8 @@ wait_for_worker_state_change(char expected_state)
 		 * waiting.
 		 */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid,
 										InvalidOid, false);
 		if (worker && worker->proc)
 			logicalrep_worker_wakeup_ptr(worker);
@@ -432,7 +435,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn)
 			 */
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-			syncworker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+			syncworker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+												MyLogicalRepWorker->subid,
 												rstate->relid, false);
 
 			if (syncworker)
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 843a878ff3..95d91bf236 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -87,11 +87,12 @@ static SQLCmd *make_sqlcmd(void);
 %token K_EXPORT_SNAPSHOT
 %token K_NOEXPORT_SNAPSHOT
 %token K_USE_SNAPSHOT
+%token K_LIST_SLOTS
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
 				create_replication_slot drop_replication_slot identify_system
-				timeline_history show sql_cmd
+				timeline_history show sql_cmd list_slots
 %type <list>	base_backup_opt_list
 %type <defelt>	base_backup_opt
 %type <uintval>	opt_timeline
@@ -124,6 +125,7 @@ command:
 			| drop_replication_slot
 			| timeline_history
 			| show
+			| list_slots
 			| sql_cmd
 			;
 
@@ -136,6 +138,15 @@ identify_system:
 					$$ = (Node *) makeNode(IdentifySystemCmd);
 				}
 			;
+/*
+ * LIST_SLOTS
+ */
+list_slots:
+			K_LIST_SLOTS
+				{
+					$$ = (Node *) makeNode(ListSlotsCmd);
+				}
+			;
 
 /*
  * SHOW setting
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 7bb501f594..2d8accd985 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -85,6 +85,7 @@ identifier		{ident_start}{ident_cont}*
 BASE_BACKUP			{ return K_BASE_BACKUP; }
 FAST			{ return K_FAST; }
 IDENTIFY_SYSTEM		{ return K_IDENTIFY_SYSTEM; }
+LIST_SLOTS		{ return K_LIST_SLOTS; }
 SHOW		{ return K_SHOW; }
 LABEL			{ return K_LABEL; }
 NOWAIT			{ return K_NOWAIT; }
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 8782bad4a2..729935fc60 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -351,7 +351,7 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
  * WAL and removal of old catalog tuples.  As decoding is done in fast_forward
  * mode, no changes are generated anyway.
  */
-static XLogRecPtr
+XLogRecPtr
 pg_logical_replication_slot_advance(XLogRecPtr moveto)
 {
 	LogicalDecodingContext *ctx;
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index d1a8113cb6..6ca304b8f5 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -426,6 +426,166 @@ IdentifySystem(void)
 	end_tup_output(tstate);
 }
 
+/*
+ * Handle the LIST_SLOTS command.
+ */
+static void
+ListSlots(void)
+{
+	DestReceiver *dest;
+	TupOutputState *tstate;
+	TupleDesc	tupdesc;
+	int			slotno;
+
+	dest = CreateDestReceiver(DestRemoteSimple);
+
+	/* need a tuple descriptor representing four columns */
+	tupdesc = CreateTemplateTupleDesc(10);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "slot_name",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "plugin",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "slot_type",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 4, "datoid",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 5, "database",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 6, "temporary",
+							  INT4OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 7, "xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 8, "catalog_xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 9, "restart_lsn",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 10, "confirmed_flush",
+							  TEXTOID, -1, 0);
+
+	/* prepare for projection of tuples */
+	tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual);
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (slotno = 0; slotno < max_replication_slots; slotno++)
+	{
+		ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno];
+		char		restart_lsn_str[MAXFNAMELEN];
+		char		confirmed_flush_lsn_str[MAXFNAMELEN];
+		Datum		values[10];
+		bool		nulls[10];
+
+		ReplicationSlotPersistency persistency;
+		TransactionId xmin;
+		TransactionId catalog_xmin;
+		XLogRecPtr	restart_lsn;
+		XLogRecPtr	confirmed_flush_lsn;
+		Oid			datoid;
+		NameData	slot_name;
+		NameData	plugin;
+		int			i;
+		int64		tmpbigint;
+
+		if (!slot->in_use)
+			continue;
+
+		SpinLockAcquire(&slot->mutex);
+
+		xmin = slot->data.xmin;
+		catalog_xmin = slot->data.catalog_xmin;
+		datoid = slot->data.database;
+		restart_lsn = slot->data.restart_lsn;
+		confirmed_flush_lsn = slot->data.confirmed_flush;
+		namecpy(&slot_name, &slot->data.name);
+		namecpy(&plugin, &slot->data.plugin);
+		persistency = slot->data.persistency;
+
+		SpinLockRelease(&slot->mutex);
+
+		memset(nulls, 0, sizeof(nulls));
+
+		i = 0;
+		values[i++] = CStringGetTextDatum(NameStr(slot_name));
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+			values[i++] = CStringGetTextDatum(NameStr(plugin));
+
+		if (datoid == InvalidOid)
+			values[i++] = CStringGetTextDatum("physical");
+		else
+			values[i++] = CStringGetTextDatum("logical");
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			tmpbigint = datoid;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			MemoryContext cur = CurrentMemoryContext;
+
+			/* syscache access needs a transaction env. */
+			StartTransactionCommand();
+			/* make dbname live outside TX context */
+			MemoryContextSwitchTo(cur);
+			values[i++] = CStringGetTextDatum(get_database_name(datoid));
+			CommitTransactionCommand();
+			/* CommitTransactionCommand switches to TopMemoryContext */
+			MemoryContextSwitchTo(cur);
+		}
+
+		values[i++] = Int32GetDatum(persistency == RS_TEMPORARY ? 1 : 0);
+
+		if (xmin != InvalidTransactionId)
+		{
+			tmpbigint = xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (catalog_xmin != InvalidTransactionId)
+		{
+			tmpbigint = catalog_xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (restart_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(restart_lsn_str, sizeof(restart_lsn_str), "%X/%X",
+					 (uint32) (restart_lsn >> 32),
+					 (uint32) restart_lsn);
+			values[i++] = CStringGetTextDatum(restart_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		if (confirmed_flush_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(confirmed_flush_lsn_str, sizeof(confirmed_flush_lsn_str),
+					 "%X/%X",
+					 (uint32) (confirmed_flush_lsn >> 32),
+					 (uint32) confirmed_flush_lsn);
+			values[i++] = CStringGetTextDatum(confirmed_flush_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		/* send it to dest */
+		do_tup_output(tstate, values, nulls);
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	end_tup_output(tstate);
+}
 
 /*
  * Handle TIMELINE_HISTORY command.
@@ -1551,6 +1711,10 @@ exec_replication_command(const char *cmd_string)
 			}
 			break;
 
+		case T_ListSlotsCmd:
+			ListSlots();
+			break;
+
 		case T_SQLCmd:
 			if (MyDatabaseId == InvalidOid)
 				ereport(ERROR,
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index cac6ff0eda..a773acab79 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -486,6 +486,7 @@ typedef enum NodeTag
 	T_StartReplicationCmd,
 	T_TimeLineHistoryCmd,
 	T_SQLCmd,
+	T_ListSlotsCmd,
 
 	/*
 	 * TAGS FOR RANDOM OTHER STUFF
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index 66c948d85e..1101f71bcc 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -33,6 +33,14 @@ typedef struct IdentifySystemCmd
 	NodeTag		type;
 } IdentifySystemCmd;
 
+/* ----------------------
+ *		LIST_SLOTS command
+ * ----------------------
+ */
+typedef struct ListSlotsCmd
+{
+	NodeTag		type;
+} ListSlotsCmd;
 
 /* ----------------------
  *		BASE_BACKUP command
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f1c10d16b8..52bdee746e 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -761,6 +761,7 @@ typedef enum
 	WAIT_EVENT_CHECKPOINTER_MAIN,
 	WAIT_EVENT_LOGICAL_APPLY_MAIN,
 	WAIT_EVENT_LOGICAL_LAUNCHER_MAIN,
+	WAIT_EVENT_REPL_SLOT_SYNC_MAIN,
 	WAIT_EVENT_PGSTAT_MAIN,
 	WAIT_EVENT_RECOVERY_WAL_ALL,
 	WAIT_EVENT_RECOVERY_WAL_STREAM,
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 7964ae254f..1a0fd44876 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -205,4 +205,7 @@ extern void CheckPointReplicationSlots(void);
 
 extern void CheckSlotRequirements(void);
 
+extern XLogRecPtr pg_logical_replication_slot_advance(XLogRecPtr moveto);
+
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 5913b580c2..9014f92bfa 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -17,6 +17,7 @@
 #include "fmgr.h"
 #include "getaddrinfo.h"		/* for NI_MAXHOST */
 #include "replication/logicalproto.h"
+#include "replication/slot.h"
 #include "replication/walsender.h"
 #include "storage/latch.h"
 #include "storage/spin.h"
@@ -167,6 +168,13 @@ typedef struct
 	}			proto;
 } WalRcvStreamOptions;
 
+/*
+ * Slot information receiver from remote.
+ *
+ * Currently this is same as ReplicationSlotPersistentData
+ */
+#define WalRecvReplicationSlotData ReplicationSlotPersistentData
+
 struct WalReceiverConn;
 typedef struct WalReceiverConn WalReceiverConn;
 
@@ -211,6 +219,7 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli,
 											int *server_version);
+typedef List *(*walrcv_list_slots_fn) (WalReceiverConn *conn);
 typedef void (*walrcv_readtimelinehistoryfile_fn) (WalReceiverConn *conn,
 												   TimeLineID tli,
 												   char **filename,
@@ -240,6 +249,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_list_slots_fn walrcv_list_slots;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
 	walrcv_endstreaming_fn walrcv_endstreaming;
@@ -262,6 +272,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli, server_version) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli, server_version)
+#define walrcv_list_slots(conn) \
+	WalReceiverFunctions->walrcv_list_slots(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
 	WalReceiverFunctions->walrcv_readtimelinehistoryfile(conn, tli, filename, content, size)
 #define walrcv_startstreaming(conn, options) \
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index ef079111cd..ba2d3f1fca 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -69,14 +69,14 @@ extern LogicalRepWorker *MyLogicalRepWorker;
 extern bool in_remote_transaction;
 
 extern void logicalrep_worker_attach(int slot);
-extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
+extern LogicalRepWorker *logicalrep_worker_find(Oid dbid, Oid subid, Oid relid,
 					   bool only_running);
 extern List *logicalrep_workers_find(Oid subid, bool only_running);
 extern void logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname,
 						 Oid userid, Oid relid);
-extern void logicalrep_worker_stop(Oid subid, Oid relid);
-extern void logicalrep_worker_stop_at_commit(Oid subid, Oid relid);
-extern void logicalrep_worker_wakeup(Oid subid, Oid relid);
+extern void logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid);
+extern void logicalrep_worker_stop_at_commit(Oid dbid, Oid subid, Oid relid);
+extern void logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid);
 extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker);
 
 extern int	logicalrep_sync_worker_count(Oid subid);
-- 
2.17.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* [PATCH v3 01/12] review docs for pg12dev
@ 2019-03-28 23:50  Justin Pryzby <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Justin Pryzby @ 2019-03-28 23:50 UTC (permalink / raw)

---
 doc/src/sgml/bloom.sgml            |  2 +-
 doc/src/sgml/catalogs.sgml         |  4 ++--
 doc/src/sgml/config.sgml           | 15 +++++++------
 doc/src/sgml/ddl.sgml              |  8 +++----
 doc/src/sgml/ecpg.sgml             | 16 ++++++--------
 doc/src/sgml/libpq.sgml            |  2 +-
 doc/src/sgml/monitoring.sgml       | 44 +++++++++++++++++++-------------------
 doc/src/sgml/perform.sgml          |  8 +++----
 doc/src/sgml/protocol.sgml         |  6 +++---
 doc/src/sgml/ref/alter_table.sgml  |  4 ++--
 doc/src/sgml/ref/create_index.sgml |  4 ++--
 doc/src/sgml/ref/pg_rewind.sgml    | 17 +++++++--------
 doc/src/sgml/ref/pgbench.sgml      |  6 ++----
 doc/src/sgml/ref/reindex.sgml      |  2 +-
 doc/src/sgml/runtime.sgml          |  7 +++---
 doc/src/sgml/sources.sgml          | 36 +++++++++++++++----------------
 doc/src/sgml/xfunc.sgml            | 12 +++++------
 src/bin/pg_upgrade/check.c         |  6 +++---
 18 files changed, 98 insertions(+), 101 deletions(-)

diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6eeadde..c341b65 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -65,7 +65,7 @@
      <para>
       Number of bits generated for each index column. Each parameter's name
       refers to the number of the index column that it controls.  The default
-      is <literal>2</literal> bits and maximum is <literal>4095</literal>.  Parameters for
+      is <literal>2</literal> bits and the maximum is <literal>4095</literal>.  Parameters for
       index columns not actually used are ignored.
      </para>
     </listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4c7e938..d544e60 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3052,7 +3052,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        simplifies <command>ATTACH/DETACH PARTITION</command> operations:
        the partition dependencies need only be added or removed.
        Example: a child partitioned index is made partition-dependent
-       on both the partition table it is on and the parent partitioned
+       on both the table partition and the parent partitioned
        index, so that it goes away if either of those is dropped, but
        not otherwise.  The dependency on the parent index is primary,
        so that if the user tries to drop the child partitioned index,
@@ -3115,7 +3115,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
    Note that it's quite possible for two objects to be linked by more than
    one <structname>pg_depend</structname> entry.  For example, a child
    partitioned index would have both a partition-type dependency on its
-   associated partition table, and an auto dependency on each column of
+   associated table partition, and an auto dependency on each column of
    that table that it indexes.  This sort of situation expresses the union
    of multiple dependency semantics.  A dependent object can be dropped
    without <literal>CASCADE</literal> if any of its dependencies satisfies
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 36062c3..73eb768 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -112,7 +112,7 @@
        For example, <literal>30.1 GB</literal> will be converted
        to <literal>30822 MB</literal> not <literal>32319628902 B</literal>.
        If the parameter is of integer type, a final rounding to integer
-       occurs after any units conversion.
+       occurs after any unit conversion.
       </para>
      </listitem>
 
@@ -3433,7 +3433,7 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
         current when the base backup was taken.  The
         value <literal>latest</literal> recovers
         to the latest timeline found in the archive, which is useful in
-        a standby server.  <literal>latest</literal> is the default.
+        a standby server.  The default is <literal>latest</literal>.
        </para>
 
        <para>
@@ -3540,7 +3540,7 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
         servers or streaming base backup clients (i.e., the maximum number of
         simultaneously running WAL sender processes). The default is
         <literal>10</literal>.  The value <literal>0</literal> means
-        replication is disabled.  Abrupt streaming client disconnection might
+        replication is disabled.  Abrupt disconnection of a streaming client might
         leave an orphaned connection slot behind until a timeout is reached,
         so this parameter should be set slightly higher than the maximum
         number of expected clients so disconnected clients can immediately
@@ -4195,8 +4195,9 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
         This parameter is intended for use with streaming replication deployments;
         however, if the parameter is specified it will be honored in all cases.
 
-        <varname>hot_standby_feedback</varname> will be delayed by use of this feature
-        which could lead to bloat on the master; use both together with care.
+        <varname>hot_standby_feedback</varname> will be delayed by use of this feature.
+        Combinining these settings could lead to bloat on the master, so should
+        be done only with care.
 
         <warning>
          <para>
@@ -6947,8 +6948,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <para>
         Causes each action executed by autovacuum to be logged if it ran for at
         least the specified number of milliseconds.  Setting this to zero logs
-        all autovacuum actions. <literal>-1</literal> (the default) disables
-        logging autovacuum actions.  For example, if you set this to
+        all autovacuum actions. <literal>-1</literal> (the default) disables logging
+        autovacuum actions.  For example, if you set this to
         <literal>250ms</literal> then all automatic vacuums and analyzes that run
         250ms or longer will be logged.  In addition, when this parameter is
         set to any value other than <literal>-1</literal>, a message will be
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index a0a7435..cfe83a8 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3867,12 +3867,12 @@ CREATE INDEX ON measurement (logdate);
 
     <para>
       Normally the set of partitions established when initially defining the
-      table are not intended to remain static.  It is common to want to
-      remove old partitions of data and periodically add new partitions for
+      table are not intended to remain static.  It's common to
+      remove partitions of old data and add partitions for
       new data. One of the most important advantages of partitioning is
-      precisely that it allows this otherwise painful task to be executed
+      allowing this otherwise painful task to be executed
       nearly instantaneously by manipulating the partition structure, rather
-      than physically moving large amounts of data around.
+      than physically moving around large amounts of data.
     </para>
 
     <para>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index 6641eee..d2dd96c 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -979,7 +979,7 @@ EXEC SQL END DECLARE SECTION;
 
     <para>
      The other way is using the <type>VARCHAR</type> type, which is a
-     special type provided by ECPG.  The definition on an array of
+     special type provided by ECPG.  The definition of an array of
      type <type>VARCHAR</type> is converted into a
      named <type>struct</type> for every variable. A declaration like:
 <programlisting>
@@ -989,7 +989,7 @@ VARCHAR var[180];
 <programlisting>
 struct varchar_var { int len; char arr[180]; } var;
 </programlisting>
-     The member <structfield>arr</structfield> hosts the string
+     The member <structfield>arr</structfield> stores the string
      including a terminating zero byte.  Thus, to store a string in
      a <type>VARCHAR</type> host variable, the host variable has to be
      declared with the length including the zero byte terminator.  The
@@ -1209,8 +1209,8 @@ EXEC SQL END DECLARE SECTION;
      <title id="ecpg-type-bytea">bytea</title>
 
      <para>
-      The handling of the <type>bytea</type> type is also similar to
-      the <type>VARCHAR</type>. The definition on an array of type
+      The handling of the <type>bytea</type> type is similar to
+      the <type>VARCHAR</type>. The definition of an array of type
       <type>bytea</type> is converted into a named struct for every
       variable. A declaration like:
 <programlisting>
@@ -1220,9 +1220,8 @@ bytea var[180];
 <programlisting>
 struct bytea_var { int len; char arr[180]; } var;
 </programlisting>
-      The member <structfield>arr</structfield> hosts binary format
-      data. It also can handle even <literal>'\0'</literal> as part of
-      data unlike <type>VARCHAR</type>.
+      The member <structfield>arr</structfield> stores binary format
+      data, which can include zero bytes, unlike <type>VARCHAR</type>.
       The data is converted from/to hex format and sent/received by
       ecpglib.
      </para>
@@ -7571,8 +7570,7 @@ PREPARE <replaceable class="parameter">name</replaceable> FROM <replaceable clas
       <listitem>
        <para>
         A literal C string or a host variable containing a preparable
-        statement, one of the SELECT, INSERT, UPDATE, or
-        DELETE.
+        statement (SELECT, INSERT, UPDATE, or DELETE).
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7f01fcc..8a8427f 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1122,7 +1122,7 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <term><literal>connect_timeout</literal></term>
       <listitem>
       <para>
-       Maximum wait for connection, in seconds (write as a decimal integer,
+       Maximum time to wait while connecting, in seconds (write as a decimal integer,
        e.g. <literal>10</literal>).  Zero, negative, or not specified means
        wait indefinitely.  The minimum allowed timeout is 2 seconds, therefore
        a value of <literal>1</literal> is interpreted as <literal>2</literal>.
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index a179d61..6c98596 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -271,13 +271,13 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
   <para>
    Some of the information in the dynamic statistics views shown in <xref
    linkend="monitoring-stats-dynamic-views-table"/> is security restricted.
-   Ordinary users can only see all the information about their own sessions
-   (sessions belonging to a role that they are a member of).  In rows about
+   Ordinary users can only see all information about their own sessions
+   (sessions belonging to a role of which they are a member).  In rows for
    other sessions, many columns will be null.  Note, however, that the
    existence of a session and its general properties such as its sessions user
    and database are visible to all users.  Superusers and members of the
    built-in role <literal>pg_read_all_stats</literal> (see also <xref
-   linkend="default-roles"/>) can see all the information about all sessions.
+   linkend="default-roles"/>) can see all information about all sessions.
   </para>
 
   <table id="monitoring-stats-dynamic-views-table">
@@ -2504,14 +2504,14 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
     <row>
      <entry><structfield>datname</structfield></entry>
      <entry><type>name</type></entry>
-     <entry>Name of this database, or <literal>NULL</literal> for the shared
+     <entry>Name of this database, or <literal>NULL</literal> for shared
      objects.</entry>
     </row>
     <row>
      <entry><structfield>numbackends</structfield></entry>
      <entry><type>integer</type></entry>
      <entry>Number of backends currently connected to this database, or
-     <literal>NULL</literal> for the shared objects.  This is the only column
+     <literal>NULL</literal> for shared objects.  This is the only column
      in this view that returns a value reflecting current state; all other
      columns return the accumulated values since the last reset.</entry>
     </row>
@@ -2633,7 +2633,7 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
   <para>
    The <structname>pg_stat_database</structname> view will contain one row
-   for each database in the cluster, plus one for the shared objects, showing
+   for each database in the cluster, plus one for shared objects, showing
    database-wide statistics.
   </para>
 
@@ -3594,16 +3594,16 @@ SELECT pg_stat_get_backend_pid(s.backendid) AS pid,
       <entry><structfield>partitions_total</structfield></entry>
       <entry><type>bigint</type></entry>
       <entry>
-       When creating an index on a partitioned table, this column is set to
-       the total number of partitions on which the index is to be created.
+       When creating an index on a partitioned table, this is
+       the total number of partitions to be processed.
       </entry>
      </row>
      <row>
       <entry><structfield>partitions_done</structfield></entry>
       <entry><type>bigint</type></entry>
       <entry>
-       When creating an index on a partitioned table, this column is set to
-       the number of partitions on which the index has been completed.
+       When creating an index on a partitioned table, this is
+       the number of partitions for which the process is complete.
       </entry>
      </row>
     </tbody>
@@ -3643,9 +3643,9 @@ SELECT pg_stat_get_backend_pid(s.backendid) AS pid,
       <entry>
        The index is being built by the access method-specific code.  In this phase,
        access methods that support progress reporting fill in their own progress data,
-       and the subphase is indicated in this column.  Typically,
+       and the subphase is indicated in this column.
        <structname>blocks_total</structname> and <structname>blocks_done</structname>
-       will contain progress data, as well as potentially
+       will contain progress data, as may
        <structname>tuples_total</structname> and <structname>tuples_done</structname>.
       </entry>
      </row>
@@ -3730,15 +3730,15 @@ SELECT pg_stat_get_backend_pid(s.backendid) AS pid,
   <title>VACUUM Progress Reporting</title>
 
   <para>
-   Whenever <command>VACUUM</command> is running, the
-   <structname>pg_stat_progress_vacuum</structname> view will contain
-   one row for each backend (including autovacuum worker processes) that is
-   currently vacuuming.  The tables below describe the information
+   The <structname>pg_stat_progress_vacuum</structname> view will contain one
+   row for each backend that is running <command>VACUUM</command>, including
+   autovacuum worker processes.
+   The tables below describe the information
    that will be reported and provide information about how to interpret it.
    Progress for <command>VACUUM FULL</command> commands is reported via
-   <structname>pg_stat_progress_cluster</structname>
-   because both <command>VACUUM FULL</command> and <command>CLUSTER</command>
-   rewrite the table, while regular <command>VACUUM</command> only modifies it
+   <structname>pg_stat_progress_cluster</structname>,
+   because <command>VACUUM FULL</command> rewrites the table, like <command>CLUSTER</command>,
+   while regular <command>VACUUM</command> only modifies it
    in place. See <xref linkend='cluster-progress-reporting'/>.
   </para>
 
@@ -3922,9 +3922,9 @@ SELECT pg_stat_get_backend_pid(s.backendid) AS pid,
   <title>CLUSTER Progress Reporting</title>
 
   <para>
-   Whenever <command>CLUSTER</command> or <command>VACUUM FULL</command> is
-   running, the <structname>pg_stat_progress_cluster</structname> view will
-   contain a row for each backend that is currently running either command.
+   The <structname>pg_stat_progress_cluster</structname> view will contain a
+   row for each backend that is running either
+   <command>CLUSTER</command> or <command>VACUUM FULL</command>.
    The tables below describe the information that will be reported and
    provide information about how to interpret it.
   </para>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a84be85..65c161b 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -899,10 +899,10 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
     Generally, the <command>EXPLAIN</command> output will display details for
     every plan node which was generated by the query planner.  However, there
     are cases where the executor is able to determine that certain nodes are
-    not required; currently, the only node types to support this are the
-    <literal>Append</literal> and <literal>MergeAppend</literal> nodes.  These
-    node types have the ability to discard subnodes which they are able to
-    determine won't contain any records required by the query.  It is possible
+    not required; currently, the only node types to support this are
+    <literal>Append</literal> and <literal>MergeAppend</literal>, which
+    are able to discard subnodes when it's deduced that
+    they will not contain any records required by the query.  It is possible
     to determine that nodes have been removed in this way by the presence of a
     "Subplans Removed" property in the <command>EXPLAIN</command> output.
    </para>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index b20f169..70f7286 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1515,7 +1515,7 @@ SELECT 1/0;
     the server will only accept encrypted packets from the client which are less
     than 16kB; <function>gss_wrap_size_limit()</function> should be used by the
     client to determine the size of the unencrypted message which will fit
-    within this limit and larger messages should be broken up into multiple
+    within this limit; larger messages should be broken up into multiple
     <function>gss_wrap()</function> calls.  Typical segments are 8kB of
     unencrypted data, resulting in encrypted packets of slightly larger than 8kB
     but well within the 16kB maximum.  The server can be expected to not send
@@ -1531,8 +1531,8 @@ SELECT 1/0;
     support to <productname>PostgreSQL</productname>.  In this case the
     connection must be closed, but the frontend might choose to open a fresh
     connection and proceed without requesting <acronym>GSSAPI</acronym>
-    encryption.  Given the length limits specified above, the ErrorMessage can
-    not be confused with a proper response from the server with an appropriate
+    encryption.  Given the length limits specified above, the ErrorMessage
+    cannot be confused with a proper response from the server with an appropriate
     length.
    </para>
 
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 49b081a..dd80bd0 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -219,7 +219,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
      <para>
       <literal>SET NOT NULL</literal> may only be applied to a column
-      providing none of the records in the table contain a
+      provided none of the records in the table contain a
       <literal>NULL</literal> value for the column.  Ordinarily this is
       checked during the <literal>ALTER TABLE</literal> by scanning the
       entire table; however, if a valid <literal>CHECK</literal> constraint is
@@ -642,7 +642,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       When applied to a partitioned table, nothing is moved, but any
       partitions created afterwards with
       <command>CREATE TABLE PARTITION OF</command> will use that tablespace,
-      unless the <literal>TABLESPACE</literal> clause is used to override it.
+      unless overridden by its <literal>TABLESPACE</literal> clause.
      </para>
 
      <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 30bb38b..bf4f550 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -181,8 +181,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, only the B-tree and GiST index access methods support this
+        feature.  In B-tree and GiST indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/ref/pg_rewind.sgml b/doc/src/sgml/ref/pg_rewind.sgml
index 4d91eeb..6f6d220 100644
--- a/doc/src/sgml/ref/pg_rewind.sgml
+++ b/doc/src/sgml/ref/pg_rewind.sgml
@@ -106,15 +106,14 @@ PostgreSQL documentation
    </para>
 
    <para>
-    <application>pg_rewind</application> will fail immediately if it finds
-    files it cannot write directly to.  This can happen for example when
-    the source and the target server use the same file mapping for read-only
-    SSL keys and certificates.  If such files are present on the target server
+    <application>pg_rewind</application> will fail immediately if it experiences
+    a write error.  This can happen for example when
+    the source and target server use the same file mapping for read-only
+    SSL keys and certificates.  If such files are present on the target server,
     it is recommended to remove them before running
-    <application>pg_rewind</application>.  After doing the rewind, some of
+    <application>pg_rewind</application>.  After performing the rewind, some of
     those files may have been copied from the source, in which case it may
-    be necessary to remove the data copied and restore back the set of links
-    used before the rewind.
+    be necessary to remove them and restore the files removed beforehand.
    </para>
   </warning>
  </refsect1>
@@ -185,7 +184,7 @@ PostgreSQL documentation
         <command>pg_rewind</command> to return without waiting, which is
         faster, but means that a subsequent operating system crash can leave
         the synchronized data folder corrupt.  Generally, this option is
-        useful for testing but should not be used when creating a production
+        useful for testing but should not be used on a production
         installation.
        </para>
       </listitem>
@@ -266,7 +265,7 @@ GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text, bigint, bigint, b
   <para>
    When executing <application>pg_rewind</application> using an online
    cluster as source which has been recently promoted, it is necessary
-   to execute a <command>CHECKPOINT</command> after promotion so as its
+   to execute a <command>CHECKPOINT</command> after promotion such that its
    control file reflects up-to-date timeline information, which is used by
    <application>pg_rewind</application> to check if the target cluster
    can be rewound using the designated source cluster.
diff --git a/doc/src/sgml/ref/pgbench.sgml b/doc/src/sgml/ref/pgbench.sgml
index ef22a48..7e8fc19 100644
--- a/doc/src/sgml/ref/pgbench.sgml
+++ b/doc/src/sgml/ref/pgbench.sgml
@@ -474,10 +474,8 @@ pgbench <optional> <replaceable>options</replaceable> </optional> <replaceable>d
            </listitem>
           </itemizedlist>
 
-        Because in "prepared" mode <application>pgbench</application> reuses
-        the parse analysis result for the second and subsequent query
-        iteration, <application>pgbench</application> runs faster in the
-        prepared mode than in other modes.
+        <application>pgbench</application> runs faster in prepared mode because the
+        parse analysis happens only during the first query.
        </para>
        <para>
         The default is simple query protocol.  (See <xref linkend="protocol"/>
diff --git a/doc/src/sgml/ref/reindex.sgml b/doc/src/sgml/ref/reindex.sgml
index 303436c..5a1f634 100644
--- a/doc/src/sgml/ref/reindex.sgml
+++ b/doc/src/sgml/ref/reindex.sgml
@@ -240,7 +240,7 @@ REINDEX [ ( VERBOSE ) ] { INDEX | TABLE | SCHEMA | DATABASE | SYSTEM } [ CONCURR
   <para>
    Reindexing a single index or table requires being the owner of that
    index or table.  Reindexing a schema or database requires being the
-   owner of that schema or database.  Note that is therefore sometimes
+   owner of that schema or database.  Note specifically that it's
    possible for non-superusers to rebuild indexes of tables owned by
    other users.  However, as a special exception, when
    <command>REINDEX DATABASE</command>, <command>REINDEX SCHEMA</command>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index e053e2e..798da30 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2634,8 +2634,9 @@ openssl x509 -req -in server.csr -text -days 365 \
    using <acronym>GSSAPI</acronym> to encrypt client/server communications for
    increased security.  Support requires that a <acronym>GSSAPI</acronym>
    implementation (such as MIT krb5) is installed on both client and server
-   systems, and that support in <productname>PostgreSQL</productname> is
-   enabled at build time (see <xref linkend="installation"/>).
+   systems, and must be enabled at the time
+   <productname>PostgreSQL</productname> is built (see <xref
+   linkend="installation"/>).
   </para>
 
   <sect2 id="gssapi-setup">
@@ -2644,7 +2645,7 @@ openssl x509 -req -in server.csr -text -days 365 \
    <para>
     The <productname>PostgreSQL</productname> server will listen for both
     normal and <acronym>GSSAPI</acronym>-encrypted connections on the same TCP
-    port, and will negotiate with any connecting client on whether to
+    port, and will negotiate with any connecting client whether to
     use <acronym>GSSAPI</acronym> for encryption (and for authentication).  By
     default, this decision is up to the client (which means it can be
     downgraded by an attacker); see <xref linkend="auth-pg-hba-conf"/> about
diff --git a/doc/src/sgml/sources.sgml b/doc/src/sgml/sources.sgml
index a339ebb..2b9f59d 100644
--- a/doc/src/sgml/sources.sgml
+++ b/doc/src/sgml/sources.sgml
@@ -103,7 +103,7 @@ less -x4
     message text.  In addition there are optional elements, the most
     common of which is an error identifier code that follows the SQL spec's
     SQLSTATE conventions.
-    <function>ereport</function> itself is just a shell function, that exists
+    <function>ereport</function> itself is just a shell function that exists
     mainly for the syntactic convenience of making message generation
     look like a function call in the C source code.  The only parameter
     accepted directly by <function>ereport</function> is the severity level.
@@ -359,7 +359,7 @@ ereport(ERROR,
      specify suppression of the <literal>CONTEXT:</literal> portion of a message in
      the postmaster log.  This should only be used for verbose debugging
      messages where the repeated inclusion of context would bloat the log
-     volume too much.
+     too much.
     </para>
    </listitem>
   </itemizedlist>
@@ -452,8 +452,8 @@ Hint:       the addendum
     enough for error messages.  Detail and hint messages can be relegated to a
     verbose mode, or perhaps a pop-up error-details window.  Also, details and
     hints would normally be suppressed from the server log to save
-    space. Reference to implementation details is best avoided since users
-    aren't expected to know the details.
+    space. References to implementation details are best avoided since users
+    aren't expected to know them.
    </para>
 
   </simplesect>
@@ -504,14 +504,14 @@ Hint:       the addendum
    <title>Use of Quotes</title>
 
    <para>
-    Use quotes always to delimit file names, user-supplied identifiers, and
+    Always use quotes to delimit file names, user-supplied identifiers, and
     other variables that might contain words.  Do not use them to mark up
     variables that will not contain words (for example, operator names).
    </para>
 
    <para>
     There are functions in the backend that will double-quote their own output
-    at need (for example, <function>format_type_be()</function>).  Do not put
+    as needed (for example, <function>format_type_be()</function>).  Do not put
     additional quotes around the output of such functions.
    </para>
 
@@ -880,16 +880,16 @@ BETTER: unrecognized node type: 42
      practices.
     </para>
     <para>
-     Features from later revision of the C standard or compiler specific
+     Features from later revisions of the C standard or compiler specific
      features can be used, if a fallback is provided.
     </para>
     <para>
-     For example <literal>_StaticAssert()</literal> and
+     For example, <literal>_StaticAssert()</literal> and
      <literal>__builtin_constant_p</literal> are currently used, even though
-     they are from newer revisions of the C standard and a
-     <productname>GCC</productname> extension respectively. If not available
-     we respectively fall back to using a C99 compatible replacement that
-     performs the same checks, but emits rather cryptic messages and do not
+     they are from a newer revision of the C standard and a
+     <productname>GCC</productname> extension, respectively. If not available, in the first case, 
+     we fall back to using a C99 compatible replacement that
+     performs the same checks, but emits rather cryptic messages; in the second case, we do not
      use <literal>__builtin_constant_p</literal>.
     </para>
    </simplesect>
@@ -939,8 +939,8 @@ MemoryContextSwitchTo(MemoryContext context)
      To be suitable to run inside a signal handler code has to be
      written very carefully. The fundamental problem is that, unless
      blocked, a signal handler can interrupt code at any time. If code
-     inside the signal handler uses the same state as code outside
-     chaos may ensue. As an example consider what happens if a signal
+     inside the signal handler uses the same state as code outside,
+     chaos may ensue. As an example, consider what happens if a signal
      handler tries to acquire a lock that's already held in the
      interrupted code.
     </para>
@@ -948,7 +948,7 @@ MemoryContextSwitchTo(MemoryContext context)
      Barring special arrangements code in signal handlers may only
      call async-signal safe functions (as defined in POSIX) and access
      variables of type <literal>volatile sig_atomic_t</literal>. A few
-     functions in <command>postgres</command> are also deemed signal safe, importantly
+     functions in <command>postgres</command> are also deemed signal safe; specifically,
      <function>SetLatch()</function>.
     </para>
     <para>
@@ -969,9 +969,9 @@ handle_sighup(SIGNAL_ARGS)
 }
 </programlisting>
      <varname>errno</varname> is saved and restored because
-     <function>SetLatch()</function> might change it. If that were not done
-     interrupted code that's currently inspecting <varname>errno</varname> might see the wrong
-     value.
+     <function>SetLatch()</function> might change it. If that were not done,
+     code interrupted by a signal might see the wrong value of
+     <varname>errno</varname>.
     </para>
    </simplesect>
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 3403269..5c17b10 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3364,11 +3364,11 @@ if (!ptr)
   </indexterm>
 
    <para>
-    By default, a function is just a <quote>black box</quote> that the
-    database system knows very little about the behavior of.  However,
-    that means that queries using the function may be executed much less
+    By default, a function is a <quote>black box</quote> that the
+    database system knows very little about, which
+    means queries calling the function may be executed less
     efficiently than they could be.  It is possible to supply additional
-    knowledge that helps the planner optimize function calls.
+    information that helps the planner optimize function calls.
    </para>
 
    <para>
@@ -3381,7 +3381,7 @@ if (!ptr)
     The parallel safety property (<literal>PARALLEL
     UNSAFE</literal>, <literal>PARALLEL RESTRICTED</literal>, or
     <literal>PARALLEL SAFE</literal>) must also be specified if you hope
-    to use the function in parallelized queries.
+    queries calling the function to use parallel query.
     It can also be useful to specify the function's estimated execution
     cost, and/or the number of rows a set-returning function is estimated
     to return.  However, the declarative way of specifying those two
@@ -3393,7 +3393,7 @@ if (!ptr)
     It is also possible to attach a <firstterm>planner support
     function</firstterm> to a SQL-callable function (called
     its <firstterm>target function</firstterm>), and thereby provide
-    knowledge about the target function that is too complex to be
+    information about the target function that is too complex to be
     represented declaratively.  Planner support functions have to be
     written in C (although their target functions might not be), so this is
     an advanced feature that relatively few people will use.
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 617270f..370c7aa 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -222,13 +222,13 @@ output_completion_banner(char *analyze_script_file_name,
 	/* Did we copy the free space files? */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) >= 804)
 		pg_log(PG_REPORT,
-			   "Optimizer statistics are not transferred by pg_upgrade so,\n"
-			   "once you start the new server, consider running:\n"
+			   "Optimizer statistics are not transferred by pg_upgrade.\n"
+			   "Once you start the new server, consider running:\n"
 			   "    %s\n\n", analyze_script_file_name);
 	else
 		pg_log(PG_REPORT,
 			   "Optimizer statistics and free space information are not transferred\n"
-			   "by pg_upgrade so, once you start the new server, consider running:\n"
+			   "by pg_upgrade.  Once you start the new server, consider running:\n"
 			   "    %s\n\n", analyze_script_file_name);
 
 
-- 
2.7.4


--cWoXeonUoKmBZSoM
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
 filename="v3-0002-Add-comma-for-readability.patch"



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2019-07-08 10:28  Thomas Munro <[email protected]>
  parent: Petr Jelinek <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Thomas Munro @ 2019-07-08 10:28 UTC (permalink / raw)
  To: Petr Jelinek <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Mon, Dec 31, 2018 at 10:23 AM Petr Jelinek
<[email protected]> wrote:
> As Andres has mentioned over at minimal decoding on standby thread [1],
> that functionality can be used to add simple worker which periodically
> synchronizes the slot state from the primary to a standby.
>
> Attached patch is rough implementation of such worker. It's nowhere near
> committable in the current state, it servers primarily two purposes - to
> have something over what we can agree on the approach (and if we do,
> serve as base for that) and to demonstrate that the patch in [1] can
> indeed be used for this functionality. All this means that this patch
> depends on the [1] to work.

Hi Petr,

Do I understand correctly that this depends on the "logical decoding
on standby" patch, but that isn't in the Commitfest?  Seems like an
oversight, since that thread has a recently posted v11 patch that
applies OK, and there was recent review.  You patches no longer apply
on top though.  Would it make sense to post a patch set here including
logical-decoding-on-standby_v11.patch + your two patches (rebased),
since this is currently marked as "Needs review"?

-- 
Thomas Munro
https://enterprisedb.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Synchronizing slots from primary to standby
@ 2021-10-31 10:08  Peter Eisentraut <[email protected]>
  0 siblings, 4 replies; 119+ messages in thread

From: Peter Eisentraut @ 2021-10-31 10:08 UTC (permalink / raw)
  To: pgsql-hackers

I want to reactivate $subject.  I took Petr Jelinek's patch from [0], 
rebased it, added a bit of testing.  It basically works, but as 
mentioned in [0], there are various issues to work out.

The idea is that the standby runs a background worker to periodically 
fetch replication slot information from the primary.  On failover, a 
logical subscriber would then ideally find up-to-date replication slots 
on the new publisher and can just continue normally.

The previous thread didn't have a lot of discussion, but I have gathered 
from off-line conversations that there is a wider agreement on this 
approach.  So the next steps would be to make it more robust and 
configurable and documented.  As I said, I added a small test case to 
show that it works at all, but I think a lot more tests should be added. 
  I have also found that this breaks some seemingly unrelated tests in 
the recovery test suite.  I have disabled these here.  I'm not sure if 
the patch actually breaks anything or if these are just differences in 
timing or implementation dependencies.  This patch adds a LIST_SLOTS 
replication command, but I think this could be replaced with just a 
SELECT FROM pg_replication_slots query now.  (This patch is originally 
older than when you could run SELECT queries over the replication protocol.)

So, again, this isn't anywhere near ready, but there is already a lot 
here to gather feedback about how it works, how it should work, how to 
configure it, and how it fits into an overall replication and HA 
architecture.


[0]: 
https://www.postgresql.org/message-id/flat/3095349b-44d4-bf11-1b33-7eefb585d578%402ndquadrant.com

From 3b3c3fa1d1e92d6b39ab0c869cb9398bf7791d48 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Sun, 31 Oct 2021 10:49:29 +0100
Subject: [PATCH v1] Synchronize logical replication slots from primary to
 standby

---
 src/backend/commands/subscriptioncmds.c       |   4 +-
 src/backend/postmaster/bgworker.c             |   3 +
 .../libpqwalreceiver/libpqwalreceiver.c       |  73 ++++
 src/backend/replication/logical/Makefile      |   1 +
 src/backend/replication/logical/launcher.c    | 199 +++++++----
 src/backend/replication/logical/slotsync.c    | 311 ++++++++++++++++++
 src/backend/replication/logical/tablesync.c   |  13 +-
 src/backend/replication/repl_gram.y           |  13 +-
 src/backend/replication/repl_scanner.l        |   1 +
 src/backend/replication/slotfuncs.c           |   2 +-
 src/backend/replication/walsender.c           | 169 +++++++++-
 src/backend/utils/activity/wait_event.c       |   3 +
 src/include/commands/subscriptioncmds.h       |   3 +
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/replnodes.h                 |   8 +
 src/include/replication/logicalworker.h       |   1 +
 src/include/replication/slot.h                |   4 +-
 src/include/replication/walreceiver.h         |  16 +
 src/include/replication/worker_internal.h     |   8 +-
 src/include/utils/wait_event.h                |   1 +
 src/test/recovery/t/007_sync_rep.pl           |   3 +-
 .../t/010_logical_decoding_timelines.pl       |   3 +-
 src/test/recovery/t/030_slot_sync.pl          |  51 +++
 23 files changed, 819 insertions(+), 72 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c
 create mode 100644 src/test/recovery/t/030_slot_sync.pl

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c47ba26369..2bab813440 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -743,7 +743,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
 
 				RemoveSubscriptionRel(sub->oid, relid);
 
-				logicalrep_worker_stop(sub->oid, relid);
+				logicalrep_worker_stop(MyDatabaseId, sub->oid, relid);
 
 				/*
 				 * For READY state, we would have already dropped the
@@ -1244,7 +1244,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	{
 		LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc);
 
-		logicalrep_worker_stop(w->subid, w->relid);
+		logicalrep_worker_stop(w->dbid, w->subid, w->relid);
 	}
 	list_free(subworkers);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index c05f500639..818b8a35e9 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -128,6 +128,9 @@ static const struct
 	},
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
+	},
+	{
+		"ReplSlotSyncMain", ReplSlotSyncMain
 	}
 };
 
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 5c6e56a5b2..5d2871eb08 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -58,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static List *libpqrcv_list_slots(WalReceiverConn *conn);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -89,6 +90,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	libpqrcv_get_conninfo,
 	libpqrcv_get_senderinfo,
 	libpqrcv_identify_system,
+	libpqrcv_list_slots,
 	libpqrcv_server_version,
 	libpqrcv_readtimelinehistoryfile,
 	libpqrcv_startstreaming,
@@ -385,6 +387,77 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get list of slots from primary.
+ */
+static List *
+libpqrcv_list_slots(WalReceiverConn *conn)
+{
+	PGresult   *res;
+	int			i;
+	List	   *slots = NIL;
+	int			ntuples;
+	WalRecvReplicationSlotData *slot_data;
+
+	res = libpqrcv_PQexec(conn->streamConn, "LIST_SLOTS");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("could not receive list of slots the primary server: %s",
+						pchomp(PQerrorMessage(conn->streamConn)))));
+	}
+	if (PQnfields(res) < 10)
+	{
+		int			nfields = PQnfields(res);
+
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("invalid response from primary server"),
+				 errdetail("Could not get list of slots: got %d fields, expected %d or more fields.",
+						   nfields, 10)));
+	}
+
+	ntuples = PQntuples(res);
+	for (i = 0; i < ntuples; i++)
+	{
+		char   *slot_type;
+
+		slot_data = palloc0(sizeof(WalRecvReplicationSlotData));
+		namestrcpy(&slot_data->name, PQgetvalue(res, i, 0));
+		if (!PQgetisnull(res, i, 1))
+			namestrcpy(&slot_data->plugin, PQgetvalue(res, i, 1));
+		slot_type = PQgetvalue(res, i, 2);
+		if (!PQgetisnull(res, i, 3))
+			slot_data->database = atooid(PQgetvalue(res, i, 3));
+		if (strcmp(slot_type, "physical") == 0)
+		{
+			if (OidIsValid(slot_data->database))
+				elog(ERROR, "unexpected physical replication slot with database set");
+		}
+		if (pg_strtoint32(PQgetvalue(res, i, 5)) == 1)
+			slot_data->persistency = RS_TEMPORARY;
+		else
+			slot_data->persistency = RS_PERSISTENT;
+		if (!PQgetisnull(res, i, 6))
+			slot_data->xmin = atooid(PQgetvalue(res, i, 6));
+		if (!PQgetisnull(res, i, 7))
+			slot_data->catalog_xmin = atooid(PQgetvalue(res, i, 7));
+		if (!PQgetisnull(res, i, 8))
+			slot_data->restart_lsn = pg_strtouint64(PQgetvalue(res, i, 8),
+													NULL, 10);
+		if (!PQgetisnull(res, i, 9))
+			slot_data->confirmed_flush = pg_strtouint64(PQgetvalue(res, i, 9),
+														NULL, 10);
+
+		slots = lappend(slots, slot_data);
+	}
+
+	PQclear(res);
+
+	return slots;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index c4e2fdeb71..bc3f23b5a2 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -24,6 +24,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 3fb4caa803..4919f74ef5 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -22,6 +22,7 @@
 #include "access/htup_details.h"
 #include "access/tableam.h"
 #include "access/xact.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 #include "funcapi.h"
@@ -212,7 +213,7 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker,
  * subscription id and relid.
  */
 LogicalRepWorker *
-logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
+logicalrep_worker_find(Oid dbid, Oid subid, Oid relid, bool only_running)
 {
 	int			i;
 	LogicalRepWorker *res = NULL;
@@ -224,8 +225,8 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
-		if (w->in_use && w->subid == subid && w->relid == relid &&
-			(!only_running || w->proc))
+		if (w->in_use && w->dbid == dbid && w->subid == subid &&
+			w->relid == relid && (!only_running || w->proc))
 		{
 			res = w;
 			break;
@@ -275,9 +276,13 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	int			nsyncworkers;
 	TimestampTz now;
 
-	ereport(DEBUG1,
-			(errmsg_internal("starting logical replication worker for subscription \"%s\"",
-							 subname)));
+	if (OidIsValid(subid))
+		ereport(DEBUG1,
+				(errmsg_internal("starting logical replication worker for subscription \"%s\"",
+								 subname)));
+	else
+		ereport(DEBUG1,
+				(errmsg("starting replication slot synchronization worker")));
 
 	/* Report this after the initial starting message for consistency. */
 	if (max_replication_slots == 0)
@@ -314,7 +319,9 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * reason we do this is because if some worker failed to start up and its
 	 * parent has crashed while waiting, the in_use state was never cleared.
 	 */
-	if (worker == NULL || nsyncworkers >= max_sync_workers_per_subscription)
+	if (worker == NULL ||
+		(OidIsValid(relid) &&
+		 nsyncworkers >= max_sync_workers_per_subscription))
 	{
 		bool		did_cleanup = false;
 
@@ -348,7 +355,7 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * silently as we might get here because of an otherwise harmless race
 	 * condition.
 	 */
-	if (nsyncworkers >= max_sync_workers_per_subscription)
+	if (OidIsValid(relid) && nsyncworkers >= max_sync_workers_per_subscription)
 	{
 		LWLockRelease(LogicalRepWorkerLock);
 		return;
@@ -395,15 +402,22 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	if (OidIsValid(subid))
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	else
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncMain");
 	if (OidIsValid(relid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u sync %u", subid, relid);
-	else
+	else if (OidIsValid(subid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u", subid);
+	else
+		snprintf(bgw.bgw_name, BGW_MAXLEN,
+				 "replication slot synchronization worker");
+
 	snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication worker");
 
 	bgw.bgw_restart_time = BGW_NEVER_RESTART;
@@ -434,14 +448,14 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
  * it detaches from the slot.
  */
 void
-logicalrep_worker_stop(Oid subid, Oid relid)
+logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 	uint16		generation;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, false);
+	worker = logicalrep_worker_find(dbid, subid, relid, false);
 
 	/* No worker, nothing to do. */
 	if (!worker)
@@ -531,13 +545,13 @@ logicalrep_worker_stop(Oid subid, Oid relid)
  * Wake up (using latch) any logical replication worker for specified sub/rel.
  */
 void
-logicalrep_worker_wakeup(Oid subid, Oid relid)
+logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, true);
+	worker = logicalrep_worker_find(dbid, subid, relid, true);
 
 	if (worker)
 		logicalrep_worker_wakeup_ptr(worker);
@@ -714,7 +728,7 @@ ApplyLauncherRegister(void)
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
 	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyLauncherMain");
 	snprintf(bgw.bgw_name, BGW_MAXLEN,
@@ -795,6 +809,113 @@ ApplyLauncherWakeup(void)
 		kill(LogicalRepCtx->launcher_pid, SIGUSR1);
 }
 
+static void
+ApplyLauncherStartSlotSync(TimestampTz *last_start_time, long *wait_time)
+{
+	WalReceiverConn *wrconn;
+	TimestampTz now;
+	char	   *err;
+	List	   *slots;
+	ListCell   *lc;
+	MemoryContext tmpctx;
+	MemoryContext oldctx;
+
+	wrconn = walrcv_connect(PrimaryConnInfo, false,
+							"Logical Replication Launcher", &err);
+	if (!wrconn)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	/* Use temporary context for the slot list and worker info. */
+	tmpctx = AllocSetContextCreate(TopMemoryContext,
+									"Logical Replication Launcher slot sync ctx",
+									ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(tmpctx);
+
+	slots = walrcv_list_slots(wrconn);
+
+	now = GetCurrentTimestamp();
+
+	foreach (lc, slots)
+	{
+		WalRecvReplicationSlotData *slot_data = lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!OidIsValid(slot_data->database))
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(slot_data->database, InvalidOid,
+								   InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(slot_data->database, InvalidOid, NULL,
+									 BOOTSTRAP_SUPERUSERID, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(tmpctx);
+
+	walrcv_disconnect(wrconn);
+}
+
+static void
+ApplyLauncherStartSubs(TimestampTz *last_start_time, long *wait_time)
+{
+	TimestampTz now;
+	List	   *sublist;
+	ListCell   *lc;
+	MemoryContext subctx;
+	MemoryContext oldctx;
+
+	now = GetCurrentTimestamp();
+
+	/* Use temporary context for the database list and worker info. */
+	subctx = AllocSetContextCreate(TopMemoryContext,
+								   "Logical Replication Launcher sublist",
+								   ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(subctx);
+
+	/* search for subscriptions to start or stop. */
+	sublist = get_subscription_list();
+
+	/* Start the missing workers for enabled subscriptions. */
+	foreach(lc, sublist)
+	{
+		Subscription *sub = (Subscription *) lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!sub->enabled)
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(sub->dbid, sub->oid, InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
+									 sub->owner, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(subctx);
+}
+
 /*
  * Main loop for the apply launcher process.
  */
@@ -822,14 +943,12 @@ ApplyLauncherMain(Datum main_arg)
 	 */
 	BackgroundWorkerInitializeConnection(NULL, NULL, 0);
 
+	load_file("libpqwalreceiver", false);
+
 	/* Enter main loop */
 	for (;;)
 	{
 		int			rc;
-		List	   *sublist;
-		ListCell   *lc;
-		MemoryContext subctx;
-		MemoryContext oldctx;
 		TimestampTz now;
 		long		wait_time = DEFAULT_NAPTIME_PER_CYCLE;
 
@@ -841,42 +960,10 @@ ApplyLauncherMain(Datum main_arg)
 		if (TimestampDifferenceExceeds(last_start_time, now,
 									   wal_retrieve_retry_interval))
 		{
-			/* Use temporary context for the database list and worker info. */
-			subctx = AllocSetContextCreate(TopMemoryContext,
-										   "Logical Replication Launcher sublist",
-										   ALLOCSET_DEFAULT_SIZES);
-			oldctx = MemoryContextSwitchTo(subctx);
-
-			/* search for subscriptions to start or stop. */
-			sublist = get_subscription_list();
-
-			/* Start the missing workers for enabled subscriptions. */
-			foreach(lc, sublist)
-			{
-				Subscription *sub = (Subscription *) lfirst(lc);
-				LogicalRepWorker *w;
-
-				if (!sub->enabled)
-					continue;
-
-				LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-				w = logicalrep_worker_find(sub->oid, InvalidOid, false);
-				LWLockRelease(LogicalRepWorkerLock);
-
-				if (w == NULL)
-				{
-					last_start_time = now;
-					wait_time = wal_retrieve_retry_interval;
-
-					logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
-											 sub->owner, InvalidOid);
-				}
-			}
-
-			/* Switch back to original memory context. */
-			MemoryContextSwitchTo(oldctx);
-			/* Clean the temporary memory. */
-			MemoryContextDelete(subctx);
+			if (!RecoveryInProgress())
+				ApplyLauncherStartSubs(&last_start_time, &wait_time);
+			else
+				ApplyLauncherStartSlotSync(&last_start_time, &wait_time);
 		}
 		else
 		{
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..9b1e56f6d8
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,311 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby from primary
+ *
+ * Copyright (c) 2016-2018, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "utils/builtins.h"
+#include "utils/pg_lsn.h"
+
+
+/*
+ * Wait for remote slot to pass localy reserved position.
+ */
+static void
+wait_for_primary_slot_catchup(WalReceiverConn *wrconn, char *slot_name,
+							  XLogRecPtr min_lsn)
+{
+	WalRcvExecResult *res;
+	TupleTableSlot *slot;
+	Oid				slotRow[1] = {LSNOID};
+	StringInfoData	cmd;
+	bool			isnull;
+	XLogRecPtr		restart_lsn;
+
+	for (;;)
+	{
+		int	rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		initStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "SELECT restart_lsn"
+						 "  FROM pg_catalog.pg_replication_slots"
+						 " WHERE slot_name = %s",
+						 quote_literal_cstr(slot_name));
+		res = walrcv_exec(wrconn, cmd.data, 1, slotRow);
+
+		if (res->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errmsg("could not fetch slot info for slot \"%s\" from primary: %s",
+							slot_name, res->err)));
+
+		slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+		if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+			ereport(ERROR,
+					(errmsg("slot \"%s\" disapeared from provider",
+							slot_name)));
+
+		restart_lsn = DatumGetLSN(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		ExecClearTuple(slot);
+		walrcv_clear_result(res);
+
+		if (restart_lsn >= min_lsn)
+			break;
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This optionally creates new slot if there is no existing one.
+ */
+static void
+synchronize_one_slot(WalReceiverConn *wrconn, char *slot_name, char *database,
+					 char *plugin_name, XLogRecPtr target_lsn)
+{
+	int			i;
+	bool		found = false;
+	XLogRecPtr	endlsn;
+
+	/* Search for the named slot and mark it active if we find it. */
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (!s->in_use)
+			continue;
+
+		if (strcmp(NameStr(s->data.name), slot_name) == 0)
+		{
+			found = true;
+			break;
+		}
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	StartTransactionCommand();
+
+	/* Already existing slot, acquire */
+	if (found)
+	{
+		ReplicationSlotAcquire(slot_name, true);
+
+		if (target_lsn < MyReplicationSlot->data.confirmed_flush)
+		{
+			elog(DEBUG1,
+				 "not synchronizing slot %s; synchronization would move it backward",
+				 slot_name);
+
+			ReplicationSlotRelease();
+			CommitTransactionCommand();
+			return;
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+		ReplicationSlot	   *slot;
+
+		ReplicationSlotCreate(slot_name, true, RS_EPHEMERAL, false);
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(database, false);
+		namestrcpy(&slot->data.plugin, plugin_name);
+		SpinLockRelease(&slot->mutex);
+
+		ReplicationSlotReserveWal();
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		if (target_lsn < MyReplicationSlot->data.restart_lsn)
+		{
+			elog(LOG, "waiting for remote slot %s lsn (%X/%X) to pass local slot lsn (%X/%X)",
+				 slot_name,
+				 (uint32) (target_lsn >> 32),
+				 (uint32) (target_lsn),
+				 (uint32) (MyReplicationSlot->data.restart_lsn >> 32),
+				 (uint32) (MyReplicationSlot->data.restart_lsn));
+
+			wait_for_primary_slot_catchup(wrconn, slot_name,
+										  MyReplicationSlot->data.restart_lsn);
+		}
+
+		ReplicationSlotPersist();
+	}
+
+	endlsn = pg_logical_replication_slot_advance(target_lsn);
+
+	elog(DEBUG3, "synchronized slot %s to lsn (%X/%X)",
+		 slot_name, (uint32) (endlsn >> 32), (uint32) (endlsn));
+
+	ReplicationSlotRelease();
+	CommitTransactionCommand();
+}
+
+static void
+synchronize_slots(void)
+{
+	WalRcvExecResult *res;
+	WalReceiverConn *wrconn = NULL;
+	TupleTableSlot *slot;
+	Oid				slotRow[3] = {TEXTOID, TEXTOID, LSNOID};
+	StringInfoData	s;
+	char		   *database;
+	char		   *err;
+	MemoryContext	oldctx = CurrentMemoryContext;
+
+	if (!WalRcv)
+		return;
+
+	/* syscache access needs a transaction env. */
+	StartTransactionCommand();
+	/* make dbname live outside TX context */
+	MemoryContextSwitchTo(oldctx);
+
+	database = get_database_name(MyDatabaseId);
+	initStringInfo(&s);
+	appendStringInfo(&s, "%s dbname=%s", PrimaryConnInfo, database);
+	wrconn = walrcv_connect(s.data, true, "slot_sync", &err);
+
+	if (wrconn == NULL)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	resetStringInfo(&s);
+	/* TODO filter slot names? */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn"
+					 "  FROM pg_catalog.pg_replication_slots"
+					 " WHERE database = %s",
+					 quote_literal_cstr(database));
+	res = walrcv_exec(wrconn, s.data, 3, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				(errmsg("could not fetch slot info from primary: %s",
+						res->err)));
+
+	CommitTransactionCommand();
+	/* CommitTransactionCommand switches to TopMemoryContext */
+	MemoryContextSwitchTo(oldctx);
+
+	slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+	{
+		char	   *slot_name;
+		char	   *plugin_name;
+		XLogRecPtr	confirmed_flush_lsn;
+		bool		isnull;
+
+		slot_name = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		plugin_name = TextDatumGetCString(slot_getattr(slot, 2, &isnull));
+		Assert(!isnull);
+
+		confirmed_flush_lsn = DatumGetLSN(slot_getattr(slot, 3, &isnull));
+		Assert(!isnull);
+
+		synchronize_one_slot(wrconn, slot_name, database, plugin_name,
+							 confirmed_flush_lsn);
+
+		ExecClearTuple(slot);
+	}
+
+	walrcv_clear_result(res);
+	pfree(database);
+
+	walrcv_disconnect(wrconn);
+}
+
+/*
+ * The main loop of our worker process.
+ */
+void
+ReplSlotSyncMain(Datum main_arg)
+{
+	int			worker_slot = DatumGetInt32(main_arg);
+
+	/* Attach to slot */
+	logicalrep_worker_attach(worker_slot);
+
+	/* Establish signal handlers. */
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	/* Connect to our database. */
+	BackgroundWorkerInitializeConnectionByOid(MyLogicalRepWorker->dbid,
+											  MyLogicalRepWorker->userid,
+											  0);
+
+	StartTransactionCommand();
+	ereport(LOG,
+			(errmsg("replication slot synchronization worker for database \"%s\" has started",
+					get_database_name(MyLogicalRepWorker->dbid))));
+	CommitTransactionCommand();
+
+	/* Main wait loop. */
+	for (;;)
+	{
+		int		rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		if (!RecoveryInProgress())
+			return;
+
+		synchronize_slots();
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f07983a43c..0e0593f716 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -100,6 +100,7 @@
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_type.h"
 #include "commands/copy.h"
+#include "commands/subscriptioncmds.h"
 #include "miscadmin.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -151,7 +152,8 @@ finish_sync_worker(void)
 	CommitTransactionCommand();
 
 	/* Find the main apply worker and signal it. */
-	logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid);
+	logicalrep_worker_wakeup(MyLogicalRepWorker->dbid,
+							 MyLogicalRepWorker->subid, InvalidOid);
 
 	/* Stop gracefully */
 	proc_exit(0);
@@ -191,7 +193,8 @@ wait_for_relation_state_change(Oid relid, char expected_state)
 
 		/* Check if the sync worker is still running and bail if not. */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid, relid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid, relid,
 										false);
 		LWLockRelease(LogicalRepWorkerLock);
 		if (!worker)
@@ -238,7 +241,8 @@ wait_for_worker_state_change(char expected_state)
 		 * waiting.
 		 */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid,
 										InvalidOid, false);
 		if (worker && worker->proc)
 			logicalrep_worker_wakeup_ptr(worker);
@@ -484,7 +488,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn)
 			 */
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-			syncworker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+			syncworker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+												MyLogicalRepWorker->subid,
 												rstate->relid, false);
 
 			if (syncworker)
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index dcb1108579..ab4869f312 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -91,11 +91,12 @@ static SQLCmd *make_sqlcmd(void);
 %token K_USE_SNAPSHOT
 %token K_MANIFEST
 %token K_MANIFEST_CHECKSUMS
+%token K_LIST_SLOTS
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
 				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show sql_cmd
+				read_replication_slot timeline_history show sql_cmd list_slots
 %type <list>	base_backup_legacy_opt_list generic_option_list
 %type <defelt>	base_backup_legacy_opt generic_option
 %type <uintval>	opt_timeline
@@ -129,6 +130,7 @@ command:
 			| read_replication_slot
 			| timeline_history
 			| show
+			| list_slots
 			| sql_cmd
 			;
 
@@ -141,6 +143,15 @@ identify_system:
 					$$ = (Node *) makeNode(IdentifySystemCmd);
 				}
 			;
+/*
+ * LIST_SLOTS
+ */
+list_slots:
+			K_LIST_SLOTS
+				{
+					$$ = (Node *) makeNode(ListSlotsCmd);
+				}
+			;
 
 /*
  * READ_REPLICATION_SLOT %s
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 1b599c255e..9ee638355d 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -85,6 +85,7 @@ identifier		{ident_start}{ident_cont}*
 BASE_BACKUP			{ return K_BASE_BACKUP; }
 FAST			{ return K_FAST; }
 IDENTIFY_SYSTEM		{ return K_IDENTIFY_SYSTEM; }
+LIST_SLOTS		{ return K_LIST_SLOTS; }
 READ_REPLICATION_SLOT	{ return K_READ_REPLICATION_SLOT; }
 SHOW		{ return K_SHOW; }
 LABEL			{ return K_LABEL; }
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 17df99c2ac..0cf1c85d52 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -484,7 +484,7 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
  * WAL and removal of old catalog tuples.  As decoding is done in fast_forward
  * mode, no changes are generated anyway.
  */
-static XLogRecPtr
+XLogRecPtr
 pg_logical_replication_slot_advance(XLogRecPtr moveto)
 {
 	LogicalDecodingContext *ctx;
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index d9ab6d6de2..f09f4c13ec 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -458,6 +458,167 @@ IdentifySystem(void)
 	end_tup_output(tstate);
 }
 
+/*
+ * Handle the LIST_SLOTS command.
+ */
+static void
+ListSlots(void)
+{
+	DestReceiver *dest;
+	TupOutputState *tstate;
+	TupleDesc	tupdesc;
+	int			slotno;
+
+	dest = CreateDestReceiver(DestRemoteSimple);
+
+	/* need a tuple descriptor representing four columns */
+	tupdesc = CreateTemplateTupleDesc(10);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "slot_name",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "plugin",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "slot_type",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 4, "datoid",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 5, "database",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 6, "temporary",
+							  INT4OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 7, "xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 8, "catalog_xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 9, "restart_lsn",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 10, "confirmed_flush",
+							  TEXTOID, -1, 0);
+
+	/* prepare for projection of tuples */
+	tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual);
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (slotno = 0; slotno < max_replication_slots; slotno++)
+	{
+		ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno];
+		char		restart_lsn_str[MAXFNAMELEN];
+		char		confirmed_flush_lsn_str[MAXFNAMELEN];
+		Datum		values[10];
+		bool		nulls[10];
+
+		ReplicationSlotPersistency persistency;
+		TransactionId xmin;
+		TransactionId catalog_xmin;
+		XLogRecPtr	restart_lsn;
+		XLogRecPtr	confirmed_flush_lsn;
+		Oid			datoid;
+		NameData	slot_name;
+		NameData	plugin;
+		int			i;
+		int64		tmpbigint;
+
+		if (!slot->in_use)
+			continue;
+
+		SpinLockAcquire(&slot->mutex);
+
+		xmin = slot->data.xmin;
+		catalog_xmin = slot->data.catalog_xmin;
+		datoid = slot->data.database;
+		restart_lsn = slot->data.restart_lsn;
+		confirmed_flush_lsn = slot->data.confirmed_flush;
+		namestrcpy(&slot_name, NameStr(slot->data.name));
+		namestrcpy(&plugin, NameStr(slot->data.plugin));
+		persistency = slot->data.persistency;
+
+		SpinLockRelease(&slot->mutex);
+
+		memset(nulls, 0, sizeof(nulls));
+
+		i = 0;
+		values[i++] = CStringGetTextDatum(NameStr(slot_name));
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+			values[i++] = CStringGetTextDatum(NameStr(plugin));
+
+		if (datoid == InvalidOid)
+			values[i++] = CStringGetTextDatum("physical");
+		else
+			values[i++] = CStringGetTextDatum("logical");
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			tmpbigint = datoid;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			MemoryContext cur = CurrentMemoryContext;
+
+			/* syscache access needs a transaction env. */
+			StartTransactionCommand();
+			/* make dbname live outside TX context */
+			MemoryContextSwitchTo(cur);
+			values[i++] = CStringGetTextDatum(get_database_name(datoid));
+			CommitTransactionCommand();
+			/* CommitTransactionCommand switches to TopMemoryContext */
+			MemoryContextSwitchTo(cur);
+		}
+
+		values[i++] = Int32GetDatum(persistency == RS_TEMPORARY ? 1 : 0);
+
+		if (xmin != InvalidTransactionId)
+		{
+			tmpbigint = xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (catalog_xmin != InvalidTransactionId)
+		{
+			tmpbigint = catalog_xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (restart_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(restart_lsn_str, sizeof(restart_lsn_str), "%X/%X",
+					 (uint32) (restart_lsn >> 32),
+					 (uint32) restart_lsn);
+			values[i++] = CStringGetTextDatum(restart_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		if (confirmed_flush_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(confirmed_flush_lsn_str, sizeof(confirmed_flush_lsn_str),
+					 "%X/%X",
+					 (uint32) (confirmed_flush_lsn >> 32),
+					 (uint32) confirmed_flush_lsn);
+			values[i++] = CStringGetTextDatum(confirmed_flush_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		/* send it to dest */
+		do_tup_output(tstate, values, nulls);
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	end_tup_output(tstate);
+}
+
 /* Handle READ_REPLICATION_SLOT command */
 static void
 ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
@@ -556,7 +717,6 @@ ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
 	end_tup_output(tstate);
 }
 
-
 /*
  * Handle TIMELINE_HISTORY command.
  */
@@ -1746,6 +1906,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_ListSlotsCmd:
+			cmdtag = "LIST_SLOTS";
+			set_ps_display(cmdtag);
+			ListSlots();
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4a5b7502f5..937be34b3d 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -230,6 +230,9 @@ pgstat_get_wait_activity(WaitEventActivity w)
 		case WAIT_EVENT_LOGICAL_LAUNCHER_MAIN:
 			event_name = "LogicalLauncherMain";
 			break;
+		case WAIT_EVENT_REPL_SLOT_SYNC_MAIN:
+			event_name = "ReplSlotSyncMain";
+			break;
 		case WAIT_EVENT_PGSTAT_MAIN:
 			event_name = "PgStatMain";
 			break;
diff --git a/src/include/commands/subscriptioncmds.h b/src/include/commands/subscriptioncmds.h
index aec7e478ab..1cc19e0c99 100644
--- a/src/include/commands/subscriptioncmds.h
+++ b/src/include/commands/subscriptioncmds.h
@@ -17,6 +17,7 @@
 
 #include "catalog/objectaddress.h"
 #include "parser/parse_node.h"
+#include "replication/walreceiver.h"
 
 extern ObjectAddress CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 										bool isTopLevel);
@@ -26,4 +27,6 @@ extern void DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel);
 extern ObjectAddress AlterSubscriptionOwner(const char *name, Oid newOwnerId);
 extern void AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId);
 
+extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
+
 #endif							/* SUBSCRIPTIONCMDS_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 7c657c1241..920a510c4c 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -501,6 +501,7 @@ typedef enum NodeTag
 	T_StartReplicationCmd,
 	T_TimeLineHistoryCmd,
 	T_SQLCmd,
+	T_ListSlotsCmd,
 
 	/*
 	 * TAGS FOR RANDOM OTHER STUFF
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index a746fafc12..06aad4fc6e 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -33,6 +33,14 @@ typedef struct IdentifySystemCmd
 	NodeTag		type;
 } IdentifySystemCmd;
 
+/* ----------------------
+ *		LIST_SLOTS command
+ * ----------------------
+ */
+typedef struct ListSlotsCmd
+{
+	NodeTag		type;
+} ListSlotsCmd;
 
 /* ----------------------
  *		BASE_BACKUP command
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index 2ad61a001a..902789f815 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -13,6 +13,7 @@
 #define LOGICALWORKER_H
 
 extern void ApplyWorkerMain(Datum main_arg);
+extern void ReplSlotSyncMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 53d773ccff..a29e517707 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -216,7 +216,6 @@ extern void ReplicationSlotsDropDBSlots(Oid dboid);
 extern bool InvalidateObsoleteReplicationSlots(XLogSegNo oldestSegno);
 extern ReplicationSlot *SearchNamedReplicationSlot(const char *name, bool need_lock);
 extern void ReplicationSlotNameForTablesync(Oid suboid, Oid relid, char *syncslotname, int szslot);
-extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
 
 extern void StartupReplicationSlots(void);
 extern void CheckPointReplicationSlots(void);
@@ -224,4 +223,7 @@ extern void CheckPointReplicationSlots(void);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern XLogRecPtr pg_logical_replication_slot_advance(XLogRecPtr moveto);
+
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0b607ed777..ff8b755c67 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -18,6 +18,7 @@
 #include "pgtime.h"
 #include "port/atomics.h"
 #include "replication/logicalproto.h"
+#include "replication/slot.h"
 #include "replication/walsender.h"
 #include "storage/condition_variable.h"
 #include "storage/latch.h"
@@ -187,6 +188,13 @@ typedef struct
 	}			proto;
 } WalRcvStreamOptions;
 
+/*
+ * Slot information receiver from remote.
+ *
+ * Currently this is same as ReplicationSlotPersistentData
+ */
+#define WalRecvReplicationSlotData ReplicationSlotPersistentData
+
 struct WalReceiverConn;
 typedef struct WalReceiverConn WalReceiverConn;
 
@@ -274,6 +282,11 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * TODO
+ */
+typedef List *(*walrcv_list_slots_fn) (WalReceiverConn *conn);
+
 /*
  * walrcv_server_version_fn
  *
@@ -387,6 +400,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_list_slots_fn walrcv_list_slots;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -411,6 +425,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_list_slots(conn) \
+	WalReceiverFunctions->walrcv_list_slots(conn)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index c00be2a2b6..e7226fcb6e 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -57,7 +57,7 @@ typedef struct LogicalRepWorker
 	 * exits.  Under this, separate buffiles would be created for each
 	 * transaction which will be deleted after the transaction is finished.
 	 */
-	FileSet    *stream_fileset;
+	struct FileSet *stream_fileset;
 
 	/* Stats. */
 	XLogRecPtr	last_lsn;
@@ -80,13 +80,13 @@ extern LogicalRepWorker *MyLogicalRepWorker;
 extern bool in_remote_transaction;
 
 extern void logicalrep_worker_attach(int slot);
-extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
+extern LogicalRepWorker *logicalrep_worker_find(Oid dbid, Oid subid, Oid relid,
 												bool only_running);
 extern List *logicalrep_workers_find(Oid subid, bool only_running);
 extern void logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname,
 									 Oid userid, Oid relid);
-extern void logicalrep_worker_stop(Oid subid, Oid relid);
-extern void logicalrep_worker_wakeup(Oid subid, Oid relid);
+extern void logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid);
+extern void logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid);
 extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker);
 
 extern int	logicalrep_sync_worker_count(Oid subid);
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index c22142365f..ba7ca743f5 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -42,6 +42,7 @@ typedef enum
 	WAIT_EVENT_CHECKPOINTER_MAIN,
 	WAIT_EVENT_LOGICAL_APPLY_MAIN,
 	WAIT_EVENT_LOGICAL_LAUNCHER_MAIN,
+	WAIT_EVENT_REPL_SLOT_SYNC_MAIN,
 	WAIT_EVENT_PGSTAT_MAIN,
 	WAIT_EVENT_RECOVERY_WAL_STREAM,
 	WAIT_EVENT_SYSLOGGER_MAIN,
diff --git a/src/test/recovery/t/007_sync_rep.pl b/src/test/recovery/t/007_sync_rep.pl
index 0d0e60b772..69504d84ee 100644
--- a/src/test/recovery/t/007_sync_rep.pl
+++ b/src/test/recovery/t/007_sync_rep.pl
@@ -6,7 +6,8 @@
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 11;
+#use Test::More tests => 11;
+use Test::More skip_all => 'FIXME';
 
 # Query checking sync_priority and sync_state of each standby
 my $check_sql =
diff --git a/src/test/recovery/t/010_logical_decoding_timelines.pl b/src/test/recovery/t/010_logical_decoding_timelines.pl
index 68d94ac91c..d45066c1f2 100644
--- a/src/test/recovery/t/010_logical_decoding_timelines.pl
+++ b/src/test/recovery/t/010_logical_decoding_timelines.pl
@@ -26,7 +26,8 @@
 
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 13;
+#use Test::More tests => 13;
+use Test::More skip_all => 'FIXME';
 use File::Copy;
 use IPC::Run ();
 use Scalar::Util qw(blessed);
diff --git a/src/test/recovery/t/030_slot_sync.pl b/src/test/recovery/t/030_slot_sync.pl
new file mode 100644
index 0000000000..109daaa9fe
--- /dev/null
+++ b/src/test/recovery/t/030_slot_sync.pl
@@ -0,0 +1,51 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 2;
+
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 'logical');
+$node_primary->start;
+
+$node_primary->backup('backup');
+
+my $node_phys_standby = PostgreSQL::Test::Cluster->new('phys_standby');
+$node_phys_standby->init_from_backup($node_primary, 'backup', has_streaming => 1);
+$node_phys_standby->start;
+
+$node_primary->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+$node_primary->safe_psql('postgres', "INSERT INTO t1 VALUES (1), (2), (3)");
+
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$node_subscriber->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+
+$node_primary->safe_psql('postgres', "CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '" . ($node_primary->connstr . ' dbname=postgres') . "' PUBLICATION pub1");
+
+# Wait for initial sync of all subscriptions
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+my $result = $node_primary->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots WHERE slot_type = 'logical'");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on primary');
+
+# FIXME: standby needs restart to pick up new slots
+$node_phys_standby->restart;
+sleep 3;
+
+$result = $node_phys_standby->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots WHERE slot_type = 'logical'");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on standby');

base-commit: e6c60719e6c6ee9bd396f430879e1de9079bf74c
-- 
2.33.1



Attachments:

  [text/plain] v1-0001-Synchronize-logical-replication-slots-from-primar.patch (44.4K, ../../[email protected]/2-v1-0001-Synchronize-logical-replication-slots-from-primar.patch)
  download | inline diff:
From 3b3c3fa1d1e92d6b39ab0c869cb9398bf7791d48 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Sun, 31 Oct 2021 10:49:29 +0100
Subject: [PATCH v1] Synchronize logical replication slots from primary to
 standby

---
 src/backend/commands/subscriptioncmds.c       |   4 +-
 src/backend/postmaster/bgworker.c             |   3 +
 .../libpqwalreceiver/libpqwalreceiver.c       |  73 ++++
 src/backend/replication/logical/Makefile      |   1 +
 src/backend/replication/logical/launcher.c    | 199 +++++++----
 src/backend/replication/logical/slotsync.c    | 311 ++++++++++++++++++
 src/backend/replication/logical/tablesync.c   |  13 +-
 src/backend/replication/repl_gram.y           |  13 +-
 src/backend/replication/repl_scanner.l        |   1 +
 src/backend/replication/slotfuncs.c           |   2 +-
 src/backend/replication/walsender.c           | 169 +++++++++-
 src/backend/utils/activity/wait_event.c       |   3 +
 src/include/commands/subscriptioncmds.h       |   3 +
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/replnodes.h                 |   8 +
 src/include/replication/logicalworker.h       |   1 +
 src/include/replication/slot.h                |   4 +-
 src/include/replication/walreceiver.h         |  16 +
 src/include/replication/worker_internal.h     |   8 +-
 src/include/utils/wait_event.h                |   1 +
 src/test/recovery/t/007_sync_rep.pl           |   3 +-
 .../t/010_logical_decoding_timelines.pl       |   3 +-
 src/test/recovery/t/030_slot_sync.pl          |  51 +++
 23 files changed, 819 insertions(+), 72 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c
 create mode 100644 src/test/recovery/t/030_slot_sync.pl

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c47ba26369..2bab813440 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -743,7 +743,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
 
 				RemoveSubscriptionRel(sub->oid, relid);
 
-				logicalrep_worker_stop(sub->oid, relid);
+				logicalrep_worker_stop(MyDatabaseId, sub->oid, relid);
 
 				/*
 				 * For READY state, we would have already dropped the
@@ -1244,7 +1244,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	{
 		LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc);
 
-		logicalrep_worker_stop(w->subid, w->relid);
+		logicalrep_worker_stop(w->dbid, w->subid, w->relid);
 	}
 	list_free(subworkers);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index c05f500639..818b8a35e9 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -128,6 +128,9 @@ static const struct
 	},
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
+	},
+	{
+		"ReplSlotSyncMain", ReplSlotSyncMain
 	}
 };
 
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 5c6e56a5b2..5d2871eb08 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -58,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static List *libpqrcv_list_slots(WalReceiverConn *conn);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -89,6 +90,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	libpqrcv_get_conninfo,
 	libpqrcv_get_senderinfo,
 	libpqrcv_identify_system,
+	libpqrcv_list_slots,
 	libpqrcv_server_version,
 	libpqrcv_readtimelinehistoryfile,
 	libpqrcv_startstreaming,
@@ -385,6 +387,77 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get list of slots from primary.
+ */
+static List *
+libpqrcv_list_slots(WalReceiverConn *conn)
+{
+	PGresult   *res;
+	int			i;
+	List	   *slots = NIL;
+	int			ntuples;
+	WalRecvReplicationSlotData *slot_data;
+
+	res = libpqrcv_PQexec(conn->streamConn, "LIST_SLOTS");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("could not receive list of slots the primary server: %s",
+						pchomp(PQerrorMessage(conn->streamConn)))));
+	}
+	if (PQnfields(res) < 10)
+	{
+		int			nfields = PQnfields(res);
+
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("invalid response from primary server"),
+				 errdetail("Could not get list of slots: got %d fields, expected %d or more fields.",
+						   nfields, 10)));
+	}
+
+	ntuples = PQntuples(res);
+	for (i = 0; i < ntuples; i++)
+	{
+		char   *slot_type;
+
+		slot_data = palloc0(sizeof(WalRecvReplicationSlotData));
+		namestrcpy(&slot_data->name, PQgetvalue(res, i, 0));
+		if (!PQgetisnull(res, i, 1))
+			namestrcpy(&slot_data->plugin, PQgetvalue(res, i, 1));
+		slot_type = PQgetvalue(res, i, 2);
+		if (!PQgetisnull(res, i, 3))
+			slot_data->database = atooid(PQgetvalue(res, i, 3));
+		if (strcmp(slot_type, "physical") == 0)
+		{
+			if (OidIsValid(slot_data->database))
+				elog(ERROR, "unexpected physical replication slot with database set");
+		}
+		if (pg_strtoint32(PQgetvalue(res, i, 5)) == 1)
+			slot_data->persistency = RS_TEMPORARY;
+		else
+			slot_data->persistency = RS_PERSISTENT;
+		if (!PQgetisnull(res, i, 6))
+			slot_data->xmin = atooid(PQgetvalue(res, i, 6));
+		if (!PQgetisnull(res, i, 7))
+			slot_data->catalog_xmin = atooid(PQgetvalue(res, i, 7));
+		if (!PQgetisnull(res, i, 8))
+			slot_data->restart_lsn = pg_strtouint64(PQgetvalue(res, i, 8),
+													NULL, 10);
+		if (!PQgetisnull(res, i, 9))
+			slot_data->confirmed_flush = pg_strtouint64(PQgetvalue(res, i, 9),
+														NULL, 10);
+
+		slots = lappend(slots, slot_data);
+	}
+
+	PQclear(res);
+
+	return slots;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index c4e2fdeb71..bc3f23b5a2 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -24,6 +24,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 3fb4caa803..4919f74ef5 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -22,6 +22,7 @@
 #include "access/htup_details.h"
 #include "access/tableam.h"
 #include "access/xact.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 #include "funcapi.h"
@@ -212,7 +213,7 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker,
  * subscription id and relid.
  */
 LogicalRepWorker *
-logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
+logicalrep_worker_find(Oid dbid, Oid subid, Oid relid, bool only_running)
 {
 	int			i;
 	LogicalRepWorker *res = NULL;
@@ -224,8 +225,8 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
-		if (w->in_use && w->subid == subid && w->relid == relid &&
-			(!only_running || w->proc))
+		if (w->in_use && w->dbid == dbid && w->subid == subid &&
+			w->relid == relid && (!only_running || w->proc))
 		{
 			res = w;
 			break;
@@ -275,9 +276,13 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	int			nsyncworkers;
 	TimestampTz now;
 
-	ereport(DEBUG1,
-			(errmsg_internal("starting logical replication worker for subscription \"%s\"",
-							 subname)));
+	if (OidIsValid(subid))
+		ereport(DEBUG1,
+				(errmsg_internal("starting logical replication worker for subscription \"%s\"",
+								 subname)));
+	else
+		ereport(DEBUG1,
+				(errmsg("starting replication slot synchronization worker")));
 
 	/* Report this after the initial starting message for consistency. */
 	if (max_replication_slots == 0)
@@ -314,7 +319,9 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * reason we do this is because if some worker failed to start up and its
 	 * parent has crashed while waiting, the in_use state was never cleared.
 	 */
-	if (worker == NULL || nsyncworkers >= max_sync_workers_per_subscription)
+	if (worker == NULL ||
+		(OidIsValid(relid) &&
+		 nsyncworkers >= max_sync_workers_per_subscription))
 	{
 		bool		did_cleanup = false;
 
@@ -348,7 +355,7 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * silently as we might get here because of an otherwise harmless race
 	 * condition.
 	 */
-	if (nsyncworkers >= max_sync_workers_per_subscription)
+	if (OidIsValid(relid) && nsyncworkers >= max_sync_workers_per_subscription)
 	{
 		LWLockRelease(LogicalRepWorkerLock);
 		return;
@@ -395,15 +402,22 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	if (OidIsValid(subid))
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	else
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncMain");
 	if (OidIsValid(relid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u sync %u", subid, relid);
-	else
+	else if (OidIsValid(subid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u", subid);
+	else
+		snprintf(bgw.bgw_name, BGW_MAXLEN,
+				 "replication slot synchronization worker");
+
 	snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication worker");
 
 	bgw.bgw_restart_time = BGW_NEVER_RESTART;
@@ -434,14 +448,14 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
  * it detaches from the slot.
  */
 void
-logicalrep_worker_stop(Oid subid, Oid relid)
+logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 	uint16		generation;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, false);
+	worker = logicalrep_worker_find(dbid, subid, relid, false);
 
 	/* No worker, nothing to do. */
 	if (!worker)
@@ -531,13 +545,13 @@ logicalrep_worker_stop(Oid subid, Oid relid)
  * Wake up (using latch) any logical replication worker for specified sub/rel.
  */
 void
-logicalrep_worker_wakeup(Oid subid, Oid relid)
+logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, true);
+	worker = logicalrep_worker_find(dbid, subid, relid, true);
 
 	if (worker)
 		logicalrep_worker_wakeup_ptr(worker);
@@ -714,7 +728,7 @@ ApplyLauncherRegister(void)
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
 	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyLauncherMain");
 	snprintf(bgw.bgw_name, BGW_MAXLEN,
@@ -795,6 +809,113 @@ ApplyLauncherWakeup(void)
 		kill(LogicalRepCtx->launcher_pid, SIGUSR1);
 }
 
+static void
+ApplyLauncherStartSlotSync(TimestampTz *last_start_time, long *wait_time)
+{
+	WalReceiverConn *wrconn;
+	TimestampTz now;
+	char	   *err;
+	List	   *slots;
+	ListCell   *lc;
+	MemoryContext tmpctx;
+	MemoryContext oldctx;
+
+	wrconn = walrcv_connect(PrimaryConnInfo, false,
+							"Logical Replication Launcher", &err);
+	if (!wrconn)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	/* Use temporary context for the slot list and worker info. */
+	tmpctx = AllocSetContextCreate(TopMemoryContext,
+									"Logical Replication Launcher slot sync ctx",
+									ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(tmpctx);
+
+	slots = walrcv_list_slots(wrconn);
+
+	now = GetCurrentTimestamp();
+
+	foreach (lc, slots)
+	{
+		WalRecvReplicationSlotData *slot_data = lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!OidIsValid(slot_data->database))
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(slot_data->database, InvalidOid,
+								   InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(slot_data->database, InvalidOid, NULL,
+									 BOOTSTRAP_SUPERUSERID, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(tmpctx);
+
+	walrcv_disconnect(wrconn);
+}
+
+static void
+ApplyLauncherStartSubs(TimestampTz *last_start_time, long *wait_time)
+{
+	TimestampTz now;
+	List	   *sublist;
+	ListCell   *lc;
+	MemoryContext subctx;
+	MemoryContext oldctx;
+
+	now = GetCurrentTimestamp();
+
+	/* Use temporary context for the database list and worker info. */
+	subctx = AllocSetContextCreate(TopMemoryContext,
+								   "Logical Replication Launcher sublist",
+								   ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(subctx);
+
+	/* search for subscriptions to start or stop. */
+	sublist = get_subscription_list();
+
+	/* Start the missing workers for enabled subscriptions. */
+	foreach(lc, sublist)
+	{
+		Subscription *sub = (Subscription *) lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!sub->enabled)
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(sub->dbid, sub->oid, InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
+									 sub->owner, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(subctx);
+}
+
 /*
  * Main loop for the apply launcher process.
  */
@@ -822,14 +943,12 @@ ApplyLauncherMain(Datum main_arg)
 	 */
 	BackgroundWorkerInitializeConnection(NULL, NULL, 0);
 
+	load_file("libpqwalreceiver", false);
+
 	/* Enter main loop */
 	for (;;)
 	{
 		int			rc;
-		List	   *sublist;
-		ListCell   *lc;
-		MemoryContext subctx;
-		MemoryContext oldctx;
 		TimestampTz now;
 		long		wait_time = DEFAULT_NAPTIME_PER_CYCLE;
 
@@ -841,42 +960,10 @@ ApplyLauncherMain(Datum main_arg)
 		if (TimestampDifferenceExceeds(last_start_time, now,
 									   wal_retrieve_retry_interval))
 		{
-			/* Use temporary context for the database list and worker info. */
-			subctx = AllocSetContextCreate(TopMemoryContext,
-										   "Logical Replication Launcher sublist",
-										   ALLOCSET_DEFAULT_SIZES);
-			oldctx = MemoryContextSwitchTo(subctx);
-
-			/* search for subscriptions to start or stop. */
-			sublist = get_subscription_list();
-
-			/* Start the missing workers for enabled subscriptions. */
-			foreach(lc, sublist)
-			{
-				Subscription *sub = (Subscription *) lfirst(lc);
-				LogicalRepWorker *w;
-
-				if (!sub->enabled)
-					continue;
-
-				LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-				w = logicalrep_worker_find(sub->oid, InvalidOid, false);
-				LWLockRelease(LogicalRepWorkerLock);
-
-				if (w == NULL)
-				{
-					last_start_time = now;
-					wait_time = wal_retrieve_retry_interval;
-
-					logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
-											 sub->owner, InvalidOid);
-				}
-			}
-
-			/* Switch back to original memory context. */
-			MemoryContextSwitchTo(oldctx);
-			/* Clean the temporary memory. */
-			MemoryContextDelete(subctx);
+			if (!RecoveryInProgress())
+				ApplyLauncherStartSubs(&last_start_time, &wait_time);
+			else
+				ApplyLauncherStartSlotSync(&last_start_time, &wait_time);
 		}
 		else
 		{
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..9b1e56f6d8
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,311 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby from primary
+ *
+ * Copyright (c) 2016-2018, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "utils/builtins.h"
+#include "utils/pg_lsn.h"
+
+
+/*
+ * Wait for remote slot to pass localy reserved position.
+ */
+static void
+wait_for_primary_slot_catchup(WalReceiverConn *wrconn, char *slot_name,
+							  XLogRecPtr min_lsn)
+{
+	WalRcvExecResult *res;
+	TupleTableSlot *slot;
+	Oid				slotRow[1] = {LSNOID};
+	StringInfoData	cmd;
+	bool			isnull;
+	XLogRecPtr		restart_lsn;
+
+	for (;;)
+	{
+		int	rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		initStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "SELECT restart_lsn"
+						 "  FROM pg_catalog.pg_replication_slots"
+						 " WHERE slot_name = %s",
+						 quote_literal_cstr(slot_name));
+		res = walrcv_exec(wrconn, cmd.data, 1, slotRow);
+
+		if (res->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errmsg("could not fetch slot info for slot \"%s\" from primary: %s",
+							slot_name, res->err)));
+
+		slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+		if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+			ereport(ERROR,
+					(errmsg("slot \"%s\" disapeared from provider",
+							slot_name)));
+
+		restart_lsn = DatumGetLSN(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		ExecClearTuple(slot);
+		walrcv_clear_result(res);
+
+		if (restart_lsn >= min_lsn)
+			break;
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This optionally creates new slot if there is no existing one.
+ */
+static void
+synchronize_one_slot(WalReceiverConn *wrconn, char *slot_name, char *database,
+					 char *plugin_name, XLogRecPtr target_lsn)
+{
+	int			i;
+	bool		found = false;
+	XLogRecPtr	endlsn;
+
+	/* Search for the named slot and mark it active if we find it. */
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (!s->in_use)
+			continue;
+
+		if (strcmp(NameStr(s->data.name), slot_name) == 0)
+		{
+			found = true;
+			break;
+		}
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	StartTransactionCommand();
+
+	/* Already existing slot, acquire */
+	if (found)
+	{
+		ReplicationSlotAcquire(slot_name, true);
+
+		if (target_lsn < MyReplicationSlot->data.confirmed_flush)
+		{
+			elog(DEBUG1,
+				 "not synchronizing slot %s; synchronization would move it backward",
+				 slot_name);
+
+			ReplicationSlotRelease();
+			CommitTransactionCommand();
+			return;
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+		ReplicationSlot	   *slot;
+
+		ReplicationSlotCreate(slot_name, true, RS_EPHEMERAL, false);
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(database, false);
+		namestrcpy(&slot->data.plugin, plugin_name);
+		SpinLockRelease(&slot->mutex);
+
+		ReplicationSlotReserveWal();
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		if (target_lsn < MyReplicationSlot->data.restart_lsn)
+		{
+			elog(LOG, "waiting for remote slot %s lsn (%X/%X) to pass local slot lsn (%X/%X)",
+				 slot_name,
+				 (uint32) (target_lsn >> 32),
+				 (uint32) (target_lsn),
+				 (uint32) (MyReplicationSlot->data.restart_lsn >> 32),
+				 (uint32) (MyReplicationSlot->data.restart_lsn));
+
+			wait_for_primary_slot_catchup(wrconn, slot_name,
+										  MyReplicationSlot->data.restart_lsn);
+		}
+
+		ReplicationSlotPersist();
+	}
+
+	endlsn = pg_logical_replication_slot_advance(target_lsn);
+
+	elog(DEBUG3, "synchronized slot %s to lsn (%X/%X)",
+		 slot_name, (uint32) (endlsn >> 32), (uint32) (endlsn));
+
+	ReplicationSlotRelease();
+	CommitTransactionCommand();
+}
+
+static void
+synchronize_slots(void)
+{
+	WalRcvExecResult *res;
+	WalReceiverConn *wrconn = NULL;
+	TupleTableSlot *slot;
+	Oid				slotRow[3] = {TEXTOID, TEXTOID, LSNOID};
+	StringInfoData	s;
+	char		   *database;
+	char		   *err;
+	MemoryContext	oldctx = CurrentMemoryContext;
+
+	if (!WalRcv)
+		return;
+
+	/* syscache access needs a transaction env. */
+	StartTransactionCommand();
+	/* make dbname live outside TX context */
+	MemoryContextSwitchTo(oldctx);
+
+	database = get_database_name(MyDatabaseId);
+	initStringInfo(&s);
+	appendStringInfo(&s, "%s dbname=%s", PrimaryConnInfo, database);
+	wrconn = walrcv_connect(s.data, true, "slot_sync", &err);
+
+	if (wrconn == NULL)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	resetStringInfo(&s);
+	/* TODO filter slot names? */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn"
+					 "  FROM pg_catalog.pg_replication_slots"
+					 " WHERE database = %s",
+					 quote_literal_cstr(database));
+	res = walrcv_exec(wrconn, s.data, 3, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				(errmsg("could not fetch slot info from primary: %s",
+						res->err)));
+
+	CommitTransactionCommand();
+	/* CommitTransactionCommand switches to TopMemoryContext */
+	MemoryContextSwitchTo(oldctx);
+
+	slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+	{
+		char	   *slot_name;
+		char	   *plugin_name;
+		XLogRecPtr	confirmed_flush_lsn;
+		bool		isnull;
+
+		slot_name = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		plugin_name = TextDatumGetCString(slot_getattr(slot, 2, &isnull));
+		Assert(!isnull);
+
+		confirmed_flush_lsn = DatumGetLSN(slot_getattr(slot, 3, &isnull));
+		Assert(!isnull);
+
+		synchronize_one_slot(wrconn, slot_name, database, plugin_name,
+							 confirmed_flush_lsn);
+
+		ExecClearTuple(slot);
+	}
+
+	walrcv_clear_result(res);
+	pfree(database);
+
+	walrcv_disconnect(wrconn);
+}
+
+/*
+ * The main loop of our worker process.
+ */
+void
+ReplSlotSyncMain(Datum main_arg)
+{
+	int			worker_slot = DatumGetInt32(main_arg);
+
+	/* Attach to slot */
+	logicalrep_worker_attach(worker_slot);
+
+	/* Establish signal handlers. */
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	/* Connect to our database. */
+	BackgroundWorkerInitializeConnectionByOid(MyLogicalRepWorker->dbid,
+											  MyLogicalRepWorker->userid,
+											  0);
+
+	StartTransactionCommand();
+	ereport(LOG,
+			(errmsg("replication slot synchronization worker for database \"%s\" has started",
+					get_database_name(MyLogicalRepWorker->dbid))));
+	CommitTransactionCommand();
+
+	/* Main wait loop. */
+	for (;;)
+	{
+		int		rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		if (!RecoveryInProgress())
+			return;
+
+		synchronize_slots();
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f07983a43c..0e0593f716 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -100,6 +100,7 @@
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_type.h"
 #include "commands/copy.h"
+#include "commands/subscriptioncmds.h"
 #include "miscadmin.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -151,7 +152,8 @@ finish_sync_worker(void)
 	CommitTransactionCommand();
 
 	/* Find the main apply worker and signal it. */
-	logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid);
+	logicalrep_worker_wakeup(MyLogicalRepWorker->dbid,
+							 MyLogicalRepWorker->subid, InvalidOid);
 
 	/* Stop gracefully */
 	proc_exit(0);
@@ -191,7 +193,8 @@ wait_for_relation_state_change(Oid relid, char expected_state)
 
 		/* Check if the sync worker is still running and bail if not. */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid, relid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid, relid,
 										false);
 		LWLockRelease(LogicalRepWorkerLock);
 		if (!worker)
@@ -238,7 +241,8 @@ wait_for_worker_state_change(char expected_state)
 		 * waiting.
 		 */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid,
 										InvalidOid, false);
 		if (worker && worker->proc)
 			logicalrep_worker_wakeup_ptr(worker);
@@ -484,7 +488,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn)
 			 */
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-			syncworker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+			syncworker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+												MyLogicalRepWorker->subid,
 												rstate->relid, false);
 
 			if (syncworker)
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index dcb1108579..ab4869f312 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -91,11 +91,12 @@ static SQLCmd *make_sqlcmd(void);
 %token K_USE_SNAPSHOT
 %token K_MANIFEST
 %token K_MANIFEST_CHECKSUMS
+%token K_LIST_SLOTS
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
 				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show sql_cmd
+				read_replication_slot timeline_history show sql_cmd list_slots
 %type <list>	base_backup_legacy_opt_list generic_option_list
 %type <defelt>	base_backup_legacy_opt generic_option
 %type <uintval>	opt_timeline
@@ -129,6 +130,7 @@ command:
 			| read_replication_slot
 			| timeline_history
 			| show
+			| list_slots
 			| sql_cmd
 			;
 
@@ -141,6 +143,15 @@ identify_system:
 					$$ = (Node *) makeNode(IdentifySystemCmd);
 				}
 			;
+/*
+ * LIST_SLOTS
+ */
+list_slots:
+			K_LIST_SLOTS
+				{
+					$$ = (Node *) makeNode(ListSlotsCmd);
+				}
+			;
 
 /*
  * READ_REPLICATION_SLOT %s
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 1b599c255e..9ee638355d 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -85,6 +85,7 @@ identifier		{ident_start}{ident_cont}*
 BASE_BACKUP			{ return K_BASE_BACKUP; }
 FAST			{ return K_FAST; }
 IDENTIFY_SYSTEM		{ return K_IDENTIFY_SYSTEM; }
+LIST_SLOTS		{ return K_LIST_SLOTS; }
 READ_REPLICATION_SLOT	{ return K_READ_REPLICATION_SLOT; }
 SHOW		{ return K_SHOW; }
 LABEL			{ return K_LABEL; }
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 17df99c2ac..0cf1c85d52 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -484,7 +484,7 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
  * WAL and removal of old catalog tuples.  As decoding is done in fast_forward
  * mode, no changes are generated anyway.
  */
-static XLogRecPtr
+XLogRecPtr
 pg_logical_replication_slot_advance(XLogRecPtr moveto)
 {
 	LogicalDecodingContext *ctx;
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index d9ab6d6de2..f09f4c13ec 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -458,6 +458,167 @@ IdentifySystem(void)
 	end_tup_output(tstate);
 }
 
+/*
+ * Handle the LIST_SLOTS command.
+ */
+static void
+ListSlots(void)
+{
+	DestReceiver *dest;
+	TupOutputState *tstate;
+	TupleDesc	tupdesc;
+	int			slotno;
+
+	dest = CreateDestReceiver(DestRemoteSimple);
+
+	/* need a tuple descriptor representing four columns */
+	tupdesc = CreateTemplateTupleDesc(10);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "slot_name",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "plugin",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "slot_type",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 4, "datoid",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 5, "database",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 6, "temporary",
+							  INT4OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 7, "xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 8, "catalog_xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 9, "restart_lsn",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 10, "confirmed_flush",
+							  TEXTOID, -1, 0);
+
+	/* prepare for projection of tuples */
+	tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual);
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (slotno = 0; slotno < max_replication_slots; slotno++)
+	{
+		ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno];
+		char		restart_lsn_str[MAXFNAMELEN];
+		char		confirmed_flush_lsn_str[MAXFNAMELEN];
+		Datum		values[10];
+		bool		nulls[10];
+
+		ReplicationSlotPersistency persistency;
+		TransactionId xmin;
+		TransactionId catalog_xmin;
+		XLogRecPtr	restart_lsn;
+		XLogRecPtr	confirmed_flush_lsn;
+		Oid			datoid;
+		NameData	slot_name;
+		NameData	plugin;
+		int			i;
+		int64		tmpbigint;
+
+		if (!slot->in_use)
+			continue;
+
+		SpinLockAcquire(&slot->mutex);
+
+		xmin = slot->data.xmin;
+		catalog_xmin = slot->data.catalog_xmin;
+		datoid = slot->data.database;
+		restart_lsn = slot->data.restart_lsn;
+		confirmed_flush_lsn = slot->data.confirmed_flush;
+		namestrcpy(&slot_name, NameStr(slot->data.name));
+		namestrcpy(&plugin, NameStr(slot->data.plugin));
+		persistency = slot->data.persistency;
+
+		SpinLockRelease(&slot->mutex);
+
+		memset(nulls, 0, sizeof(nulls));
+
+		i = 0;
+		values[i++] = CStringGetTextDatum(NameStr(slot_name));
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+			values[i++] = CStringGetTextDatum(NameStr(plugin));
+
+		if (datoid == InvalidOid)
+			values[i++] = CStringGetTextDatum("physical");
+		else
+			values[i++] = CStringGetTextDatum("logical");
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			tmpbigint = datoid;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			MemoryContext cur = CurrentMemoryContext;
+
+			/* syscache access needs a transaction env. */
+			StartTransactionCommand();
+			/* make dbname live outside TX context */
+			MemoryContextSwitchTo(cur);
+			values[i++] = CStringGetTextDatum(get_database_name(datoid));
+			CommitTransactionCommand();
+			/* CommitTransactionCommand switches to TopMemoryContext */
+			MemoryContextSwitchTo(cur);
+		}
+
+		values[i++] = Int32GetDatum(persistency == RS_TEMPORARY ? 1 : 0);
+
+		if (xmin != InvalidTransactionId)
+		{
+			tmpbigint = xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (catalog_xmin != InvalidTransactionId)
+		{
+			tmpbigint = catalog_xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (restart_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(restart_lsn_str, sizeof(restart_lsn_str), "%X/%X",
+					 (uint32) (restart_lsn >> 32),
+					 (uint32) restart_lsn);
+			values[i++] = CStringGetTextDatum(restart_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		if (confirmed_flush_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(confirmed_flush_lsn_str, sizeof(confirmed_flush_lsn_str),
+					 "%X/%X",
+					 (uint32) (confirmed_flush_lsn >> 32),
+					 (uint32) confirmed_flush_lsn);
+			values[i++] = CStringGetTextDatum(confirmed_flush_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		/* send it to dest */
+		do_tup_output(tstate, values, nulls);
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	end_tup_output(tstate);
+}
+
 /* Handle READ_REPLICATION_SLOT command */
 static void
 ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
@@ -556,7 +717,6 @@ ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
 	end_tup_output(tstate);
 }
 
-
 /*
  * Handle TIMELINE_HISTORY command.
  */
@@ -1746,6 +1906,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_ListSlotsCmd:
+			cmdtag = "LIST_SLOTS";
+			set_ps_display(cmdtag);
+			ListSlots();
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4a5b7502f5..937be34b3d 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -230,6 +230,9 @@ pgstat_get_wait_activity(WaitEventActivity w)
 		case WAIT_EVENT_LOGICAL_LAUNCHER_MAIN:
 			event_name = "LogicalLauncherMain";
 			break;
+		case WAIT_EVENT_REPL_SLOT_SYNC_MAIN:
+			event_name = "ReplSlotSyncMain";
+			break;
 		case WAIT_EVENT_PGSTAT_MAIN:
 			event_name = "PgStatMain";
 			break;
diff --git a/src/include/commands/subscriptioncmds.h b/src/include/commands/subscriptioncmds.h
index aec7e478ab..1cc19e0c99 100644
--- a/src/include/commands/subscriptioncmds.h
+++ b/src/include/commands/subscriptioncmds.h
@@ -17,6 +17,7 @@
 
 #include "catalog/objectaddress.h"
 #include "parser/parse_node.h"
+#include "replication/walreceiver.h"
 
 extern ObjectAddress CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 										bool isTopLevel);
@@ -26,4 +27,6 @@ extern void DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel);
 extern ObjectAddress AlterSubscriptionOwner(const char *name, Oid newOwnerId);
 extern void AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId);
 
+extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
+
 #endif							/* SUBSCRIPTIONCMDS_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 7c657c1241..920a510c4c 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -501,6 +501,7 @@ typedef enum NodeTag
 	T_StartReplicationCmd,
 	T_TimeLineHistoryCmd,
 	T_SQLCmd,
+	T_ListSlotsCmd,
 
 	/*
 	 * TAGS FOR RANDOM OTHER STUFF
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index a746fafc12..06aad4fc6e 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -33,6 +33,14 @@ typedef struct IdentifySystemCmd
 	NodeTag		type;
 } IdentifySystemCmd;
 
+/* ----------------------
+ *		LIST_SLOTS command
+ * ----------------------
+ */
+typedef struct ListSlotsCmd
+{
+	NodeTag		type;
+} ListSlotsCmd;
 
 /* ----------------------
  *		BASE_BACKUP command
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index 2ad61a001a..902789f815 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -13,6 +13,7 @@
 #define LOGICALWORKER_H
 
 extern void ApplyWorkerMain(Datum main_arg);
+extern void ReplSlotSyncMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 53d773ccff..a29e517707 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -216,7 +216,6 @@ extern void ReplicationSlotsDropDBSlots(Oid dboid);
 extern bool InvalidateObsoleteReplicationSlots(XLogSegNo oldestSegno);
 extern ReplicationSlot *SearchNamedReplicationSlot(const char *name, bool need_lock);
 extern void ReplicationSlotNameForTablesync(Oid suboid, Oid relid, char *syncslotname, int szslot);
-extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
 
 extern void StartupReplicationSlots(void);
 extern void CheckPointReplicationSlots(void);
@@ -224,4 +223,7 @@ extern void CheckPointReplicationSlots(void);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern XLogRecPtr pg_logical_replication_slot_advance(XLogRecPtr moveto);
+
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0b607ed777..ff8b755c67 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -18,6 +18,7 @@
 #include "pgtime.h"
 #include "port/atomics.h"
 #include "replication/logicalproto.h"
+#include "replication/slot.h"
 #include "replication/walsender.h"
 #include "storage/condition_variable.h"
 #include "storage/latch.h"
@@ -187,6 +188,13 @@ typedef struct
 	}			proto;
 } WalRcvStreamOptions;
 
+/*
+ * Slot information receiver from remote.
+ *
+ * Currently this is same as ReplicationSlotPersistentData
+ */
+#define WalRecvReplicationSlotData ReplicationSlotPersistentData
+
 struct WalReceiverConn;
 typedef struct WalReceiverConn WalReceiverConn;
 
@@ -274,6 +282,11 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * TODO
+ */
+typedef List *(*walrcv_list_slots_fn) (WalReceiverConn *conn);
+
 /*
  * walrcv_server_version_fn
  *
@@ -387,6 +400,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_list_slots_fn walrcv_list_slots;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -411,6 +425,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_list_slots(conn) \
+	WalReceiverFunctions->walrcv_list_slots(conn)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index c00be2a2b6..e7226fcb6e 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -57,7 +57,7 @@ typedef struct LogicalRepWorker
 	 * exits.  Under this, separate buffiles would be created for each
 	 * transaction which will be deleted after the transaction is finished.
 	 */
-	FileSet    *stream_fileset;
+	struct FileSet *stream_fileset;
 
 	/* Stats. */
 	XLogRecPtr	last_lsn;
@@ -80,13 +80,13 @@ extern LogicalRepWorker *MyLogicalRepWorker;
 extern bool in_remote_transaction;
 
 extern void logicalrep_worker_attach(int slot);
-extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
+extern LogicalRepWorker *logicalrep_worker_find(Oid dbid, Oid subid, Oid relid,
 												bool only_running);
 extern List *logicalrep_workers_find(Oid subid, bool only_running);
 extern void logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname,
 									 Oid userid, Oid relid);
-extern void logicalrep_worker_stop(Oid subid, Oid relid);
-extern void logicalrep_worker_wakeup(Oid subid, Oid relid);
+extern void logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid);
+extern void logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid);
 extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker);
 
 extern int	logicalrep_sync_worker_count(Oid subid);
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index c22142365f..ba7ca743f5 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -42,6 +42,7 @@ typedef enum
 	WAIT_EVENT_CHECKPOINTER_MAIN,
 	WAIT_EVENT_LOGICAL_APPLY_MAIN,
 	WAIT_EVENT_LOGICAL_LAUNCHER_MAIN,
+	WAIT_EVENT_REPL_SLOT_SYNC_MAIN,
 	WAIT_EVENT_PGSTAT_MAIN,
 	WAIT_EVENT_RECOVERY_WAL_STREAM,
 	WAIT_EVENT_SYSLOGGER_MAIN,
diff --git a/src/test/recovery/t/007_sync_rep.pl b/src/test/recovery/t/007_sync_rep.pl
index 0d0e60b772..69504d84ee 100644
--- a/src/test/recovery/t/007_sync_rep.pl
+++ b/src/test/recovery/t/007_sync_rep.pl
@@ -6,7 +6,8 @@
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 11;
+#use Test::More tests => 11;
+use Test::More skip_all => 'FIXME';
 
 # Query checking sync_priority and sync_state of each standby
 my $check_sql =
diff --git a/src/test/recovery/t/010_logical_decoding_timelines.pl b/src/test/recovery/t/010_logical_decoding_timelines.pl
index 68d94ac91c..d45066c1f2 100644
--- a/src/test/recovery/t/010_logical_decoding_timelines.pl
+++ b/src/test/recovery/t/010_logical_decoding_timelines.pl
@@ -26,7 +26,8 @@
 
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 13;
+#use Test::More tests => 13;
+use Test::More skip_all => 'FIXME';
 use File::Copy;
 use IPC::Run ();
 use Scalar::Util qw(blessed);
diff --git a/src/test/recovery/t/030_slot_sync.pl b/src/test/recovery/t/030_slot_sync.pl
new file mode 100644
index 0000000000..109daaa9fe
--- /dev/null
+++ b/src/test/recovery/t/030_slot_sync.pl
@@ -0,0 +1,51 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 2;
+
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 'logical');
+$node_primary->start;
+
+$node_primary->backup('backup');
+
+my $node_phys_standby = PostgreSQL::Test::Cluster->new('phys_standby');
+$node_phys_standby->init_from_backup($node_primary, 'backup', has_streaming => 1);
+$node_phys_standby->start;
+
+$node_primary->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+$node_primary->safe_psql('postgres', "INSERT INTO t1 VALUES (1), (2), (3)");
+
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$node_subscriber->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+
+$node_primary->safe_psql('postgres', "CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '" . ($node_primary->connstr . ' dbname=postgres') . "' PUBLICATION pub1");
+
+# Wait for initial sync of all subscriptions
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+my $result = $node_primary->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots WHERE slot_type = 'logical'");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on primary');
+
+# FIXME: standby needs restart to pick up new slots
+$node_phys_standby->restart;
+sleep 3;
+
+$result = $node_phys_standby->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots WHERE slot_type = 'logical'");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on standby');

base-commit: e6c60719e6c6ee9bd396f430879e1de9079bf74c
-- 
2.33.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-24 06:11  Masahiko Sawada <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  3 siblings, 1 reply; 119+ messages in thread

From: Masahiko Sawada @ 2021-11-24 06:11 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers

On Sun, Oct 31, 2021 at 7:08 PM Peter Eisentraut
<[email protected]> wrote:
>
> I want to reactivate $subject.  I took Petr Jelinek's patch from [0],
> rebased it, added a bit of testing.  It basically works, but as
> mentioned in [0], there are various issues to work out.

Thank you for working on this feature!

>
> The idea is that the standby runs a background worker to periodically
> fetch replication slot information from the primary.  On failover, a
> logical subscriber would then ideally find up-to-date replication slots
> on the new publisher and can just continue normally.
>
>   I have also found that this breaks some seemingly unrelated tests in
> the recovery test suite.  I have disabled these here.  I'm not sure if
> the patch actually breaks anything or if these are just differences in
> timing or implementation dependencies.

I haven’t looked at the patch deeply but regarding 007_sync_rep.pl,
the tests seem to fail since the tests rely on the order of the wal
sender array on the shared memory. Since a background worker for
synchronizing replication slots periodically connects to the walsender
on the primary and disconnects, it breaks the assumption of the order.
Regarding 010_logical_decoding_timelines.pl, I guess that the patch
breaks the test because the background worker for synchronizing slots
on the replica periodically advances the replica's slot. I think we
need to have a way to disable the slot synchronization or to specify
the slot name to sync with the primary. I'm not sure we already
discussed this topic but I think we need it at least for testing
purposes.

Regards,

--
Masahiko Sawada
EDB:  https://www.enterprisedb.com/





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-24 16:25  Dimitri Fontaine <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  3 siblings, 1 reply; 119+ messages in thread

From: Dimitri Fontaine @ 2021-11-24 16:25 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers

Hi all,

Peter Eisentraut <[email protected]> writes:
> I want to reactivate $subject.  I took Petr Jelinek's patch from [0],
> rebased it, added a bit of testing.  It basically works, but as 
> mentioned in [0], there are various issues to work out.

Thanks for working on that topic, I believe it's an important part of
Postgres' HA story.

> The idea is that the standby runs a background worker to periodically fetch
> replication slot information from the primary.  On failover, a logical
> subscriber would then ideally find up-to-date replication slots on the new
> publisher and can just continue normally.

Is there a case to be made about doing the same thing for physical
replication slots too?

That's what pg_auto_failover [1] does by default: it creates replication
slots on every node for every other node, in a way that a standby
Postgres instance now maintains a replication slot for the primary. This
ensures that after a promotion, the standby knows to retain any and all
WAL segments that the primary might need when rejoining, at pg_rewind
time.

> The previous thread didn't have a lot of discussion, but I have gathered
> from off-line conversations that there is a wider agreement on this 
> approach.  So the next steps would be to make it more robust and
> configurable and documented.

I suppose part of the configuration would then include taking care of
physical slots. Some people might want to turn that off and use the
Postgres 13+ ability to use the remote primary restore_command to fetch
missing WAL files, instead. Well, people who have setup an archiving
system, anyway.

> As I said, I added a small test case to 
> show that it works at all, but I think a lot more tests should be added.   I
> have also found that this breaks some seemingly unrelated tests in the
> recovery test suite.  I have disabled these here.  I'm not sure if the patch
> actually breaks anything or if these are just differences in timing or
> implementation dependencies.  This patch adds a LIST_SLOTS replication
> command, but I think this could be replaced with just a SELECT FROM
> pg_replication_slots query now.  (This patch is originally older than when
> you could run SELECT queries over the replication protocol.)

Given the admitted state of the patch, I didn't focus on tests. I could
successfully apply the patch on-top of current master's branch, and
cleanly compile and `make check`.

Then I also updated pg_auto_failover to support Postgres 15devel [2] so
that I could then `make NODES=3 cluster` there and play with the new
replication command:

  $ psql -d "port=5501 replication=1" -c "LIST_SLOTS;"
  psql:/Users/dim/.psqlrc:24: ERROR:  XX000: cannot execute SQL commands in WAL sender for physical replication
  LOCATION:  exec_replication_command, walsender.c:1830
  ...

I'm not too sure about this idea of running SQL in a replication
protocol connection that you're mentioning, but I suppose that's just me
needing to brush up on the topic.

> So, again, this isn't anywhere near ready, but there is already a lot here
> to gather feedback about how it works, how it should work, how to configure
> it, and how it fits into an overall replication and HA architecture.

Maybe the first question about configuration would be about selecting
which slots a standby should maintain from the primary. Is it all of the
slots that exists on both the nodes, or a sublist of that?

Is it possible to have a slot with the same name on a primary and a
standby node, in a way that the standby's slot would be a completely
separate entity from the primary's slot? If yes (I just don't know at
the moment), well then, should we continue to allow that?

Other aspects of the configuration might include a list of databases in
which to make the new background worker active, and the polling delay,
etc.

Also, do we want to even consider having the slot management on a
primary node depend on the ability to sync the advancing on one or more
standby nodes? I'm not sure to see that one as a good idea, but maybe we
want to kill it publically very early then ;-)

Regards,
-- 
dim

Author of “The Art of PostgreSQL”, see https://theartofpostgresql.com

[1]: https://github.com/citusdata/pg_auto_failover
[2]: https://github.com/citusdata/pg_auto_failover/pull/838





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-28 06:52  Bharath Rupireddy <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  3 siblings, 2 replies; 119+ messages in thread

From: Bharath Rupireddy @ 2021-11-28 06:52 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers

On Sun, Oct 31, 2021 at 3:38 PM Peter Eisentraut
<[email protected]> wrote:
>
> I want to reactivate $subject.  I took Petr Jelinek's patch from [0],
> rebased it, added a bit of testing.  It basically works, but as
> mentioned in [0], there are various issues to work out.
>
> The idea is that the standby runs a background worker to periodically
> fetch replication slot information from the primary.  On failover, a
> logical subscriber would then ideally find up-to-date replication slots
> on the new publisher and can just continue normally.
>
> The previous thread didn't have a lot of discussion, but I have gathered
> from off-line conversations that there is a wider agreement on this
> approach.  So the next steps would be to make it more robust and
> configurable and documented.  As I said, I added a small test case to
> show that it works at all, but I think a lot more tests should be added.
>   I have also found that this breaks some seemingly unrelated tests in
> the recovery test suite.  I have disabled these here.  I'm not sure if
> the patch actually breaks anything or if these are just differences in
> timing or implementation dependencies.  This patch adds a LIST_SLOTS
> replication command, but I think this could be replaced with just a
> SELECT FROM pg_replication_slots query now.  (This patch is originally
> older than when you could run SELECT queries over the replication protocol.)
>
> So, again, this isn't anywhere near ready, but there is already a lot
> here to gather feedback about how it works, how it should work, how to
> configure it, and how it fits into an overall replication and HA
> architecture.
>
>
> [0]:
> https://www.postgresql.org/message-id/flat/3095349b-44d4-bf11-1b33-7eefb585d578%402ndquadrant.com

Thanks for working on this patch. This feature will be useful as it
avoids manual intervention during the failover.

Here are some thoughts:
1) Instead of a new LIST_SLOT command, can't we use
READ_REPLICATION_SLOT (slight modifications needs to be done to make
it support logical replication slots and to get more information from
the subscriber).

2) How frequently the new bg worker is going to sync the slot info?
How can it ensure that the latest information exists say when the
subscriber is down/crashed before it picks up the latest slot
information?

3) Instead of the subscriber pulling the slot info, why can't the
publisher (via the walsender or a new bg worker maybe?) push the
latest slot info? I'm not sure we want to add more functionality to
the walsender, if yes, isn't it going to be much simpler?

4) IIUC, the proposal works only for logical replication slots but do
you also see the need for supporting some kind of synchronization of
physical replication slots as well? IMO, we need a better and
consistent way for both types of replication slots. If the walsender
can somehow push the slot info from the primary (for physical
replication slots)/publisher (for logical replication slots) to the
standby/subscribers, this will be a more consistent and simplistic
design. However, I'm not sure if this design is doable at all.

Regards,
Bharath Rupireddy.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-28 20:17  SATYANARAYANA NARLAPURAM <[email protected]>
  parent: Bharath Rupireddy <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: SATYANARAYANA NARLAPURAM @ 2021-11-28 20:17 UTC (permalink / raw)
  To: Bharath Rupireddy <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; pgsql-hackers

> 3) Instead of the subscriber pulling the slot info, why can't the
> publisher (via the walsender or a new bg worker maybe?) push the
> latest slot info? I'm not sure we want to add more functionality to
> the walsender, if yes, isn't it going to be much simpler?
>

Standby pulling the information or at least making a first attempt to
connect to the  primary is a better design as primary doesn't need to spend
its cycles repeatedly connecting to an unreachable standby. In fact,
primary wouldn't even need to know the followers, for example followers /
log shipping standbys


^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-29 04:09  Bharath Rupireddy <[email protected]>
  parent: SATYANARAYANA NARLAPURAM <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Bharath Rupireddy @ 2021-11-29 04:09 UTC (permalink / raw)
  To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; pgsql-hackers

On Mon, Nov 29, 2021 at 1:48 AM SATYANARAYANA NARLAPURAM
<[email protected]> wrote:
>
>> 3) Instead of the subscriber pulling the slot info, why can't the
>> publisher (via the walsender or a new bg worker maybe?) push the
>> latest slot info? I'm not sure we want to add more functionality to
>> the walsender, if yes, isn't it going to be much simpler?
>
> Standby pulling the information or at least making a first attempt to connect to the  primary is a better design as primary doesn't need to spend its cycles repeatedly connecting to an unreachable standby. In fact, primary wouldn't even need to know the followers, for example followers / log shipping standbys

My idea was to let the existing walsender from the primary/publisher
to send the slot info (both logical and physical replication slots) to
the standby/subscriber, probably by piggybacking the slot info with
the WAL currently it sends. Having said that, I don't know the
feasibility of it. Anyways, I'm not in favour of having a new bg
worker to just ship the slot info. The standby/subscriber, while
making connection to primary/publisher, can choose to get the
replication slot info.

As I said upthread, the problem I see with standby/subscriber pulling
the info is that: how frequently the standby/subscriber is going to
sync the slot info from primary/publisher? How can it ensure that the
latest information exists say when the subscriber is down/crashed
before it picks up the latest slot information?

IIUC, the initial idea proposed in this patch deals with only logical
replication slots not the physical replication slots, what I'm
thinking is to have a generic way to deal with both of them.

Note: In the above description, I used primary-standby and
publisher-subscriber to represent the physical and logical replication
slots respectively.

Regards,
Bharath Rupireddy.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-29 05:44  Dilip Kumar <[email protected]>
  parent: Bharath Rupireddy <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Dilip Kumar @ 2021-11-29 05:44 UTC (permalink / raw)
  To: Bharath Rupireddy <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Peter Eisentraut <[email protected]>; pgsql-hackers

On Mon, Nov 29, 2021 at 9:40 AM Bharath Rupireddy
<[email protected]> wrote:
>
> On Mon, Nov 29, 2021 at 1:48 AM SATYANARAYANA NARLAPURAM
> <[email protected]> wrote:
> >
> >> 3) Instead of the subscriber pulling the slot info, why can't the
> >> publisher (via the walsender or a new bg worker maybe?) push the
> >> latest slot info? I'm not sure we want to add more functionality to
> >> the walsender, if yes, isn't it going to be much simpler?
> >
> > Standby pulling the information or at least making a first attempt to connect to the  primary is a better design as primary doesn't need to spend its cycles repeatedly connecting to an unreachable standby. In fact, primary wouldn't even need to know the followers, for example followers / log shipping standbys
>
> My idea was to let the existing walsender from the primary/publisher
> to send the slot info (both logical and physical replication slots) to
> the standby/subscriber, probably by piggybacking the slot info with
> the WAL currently it sends. Having said that, I don't know the
> feasibility of it. Anyways, I'm not in favour of having a new bg
> worker to just ship the slot info. The standby/subscriber, while
> making connection to primary/publisher, can choose to get the
> replication slot info.

I think it is possible that the standby is restoring the WAL directly
from the archive location and there might not be any wal sender at
time. So I think the idea of standby pulling the WAL looks better to
me.

> As I said upthread, the problem I see with standby/subscriber pulling
> the info is that: how frequently the standby/subscriber is going to
> sync the slot info from primary/publisher? How can it ensure that the
> latest information exists say when the subscriber is down/crashed
> before it picks up the latest slot information?

Yeah that is a good question that how frequently the subscriber should
fetch the slot information, I think that should be configurable
values.  And the time delay is more, the chances of losing the latest
slot is more.

-- 
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-29 06:49  Bharath Rupireddy <[email protected]>
  parent: Dilip Kumar <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Bharath Rupireddy @ 2021-11-29 06:49 UTC (permalink / raw)
  To: Dilip Kumar <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Peter Eisentraut <[email protected]>; pgsql-hackers

On Mon, Nov 29, 2021 at 11:14 AM Dilip Kumar <[email protected]> wrote:
>
> On Mon, Nov 29, 2021 at 9:40 AM Bharath Rupireddy
> <[email protected]> wrote:
> >
> > On Mon, Nov 29, 2021 at 1:48 AM SATYANARAYANA NARLAPURAM
> > <[email protected]> wrote:
> > >
> > >> 3) Instead of the subscriber pulling the slot info, why can't the
> > >> publisher (via the walsender or a new bg worker maybe?) push the
> > >> latest slot info? I'm not sure we want to add more functionality to
> > >> the walsender, if yes, isn't it going to be much simpler?
> > >
> > > Standby pulling the information or at least making a first attempt to connect to the  primary is a better design as primary doesn't need to spend its cycles repeatedly connecting to an unreachable standby. In fact, primary wouldn't even need to know the followers, for example followers / log shipping standbys
> >
> > My idea was to let the existing walsender from the primary/publisher
> > to send the slot info (both logical and physical replication slots) to
> > the standby/subscriber, probably by piggybacking the slot info with
> > the WAL currently it sends. Having said that, I don't know the
> > feasibility of it. Anyways, I'm not in favour of having a new bg
> > worker to just ship the slot info. The standby/subscriber, while
> > making connection to primary/publisher, can choose to get the
> > replication slot info.
>
> I think it is possible that the standby is restoring the WAL directly
> from the archive location and there might not be any wal sender at
> time. So I think the idea of standby pulling the WAL looks better to
> me.

My point was that why can't we let the walreceiver (of course users
can configure it on the standby/subscriber) to choose whether or not
to receive the replication (both physical and logical) slot info from
the primary/publisher and if yes, the walsender(on the
primary/publisher) sending it probably as a new WAL record or just
piggybacking the replication slot info with any of the existing WAL
records.

Or simply a common bg worker (as opposed to the bg worker proposed
originally in this thread which, IIUC, works for logical replication)
running on standby/subscriber for getting both the physical and
logical replication slots info.

> > As I said upthread, the problem I see with standby/subscriber pulling
> > the info is that: how frequently the standby/subscriber is going to
> > sync the slot info from primary/publisher? How can it ensure that the
> > latest information exists say when the subscriber is down/crashed
> > before it picks up the latest slot information?
>
> Yeah that is a good question that how frequently the subscriber should
> fetch the slot information, I think that should be configurable
> values.  And the time delay is more, the chances of losing the latest
> slot is more.

I agree that it should be configurable. Even if the primary/publisher
is down/crashed, one can still compare the latest slot info from both
the primary/publisher and standby/subscriber using a new tool
pg_replslotdata proposed at [1] and see how far and which slots missed
the latest replication slot info and probably drop those alone to
recreate and retain other slots as is.

[1] - https://www.postgresql.org/message-id/CALj2ACW0rV5gWK8A3m6_X62qH%2BVfaq5hznC%3Di0R5Wojt5%2Byhyw%40ma...

Regards,
Bharath Rupireddy.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-29 07:39  Dilip Kumar <[email protected]>
  parent: Bharath Rupireddy <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Dilip Kumar @ 2021-11-29 07:39 UTC (permalink / raw)
  To: Bharath Rupireddy <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Peter Eisentraut <[email protected]>; pgsql-hackers

On Mon, Nov 29, 2021 at 12:19 PM Bharath Rupireddy
<[email protected]> wrote:
>
> On Mon, Nov 29, 2021 at 11:14 AM Dilip Kumar <[email protected]> wrote:
> >
> > On Mon, Nov 29, 2021 at 9:40 AM Bharath Rupireddy
> > <[email protected]> wrote:
> > >
> > > On Mon, Nov 29, 2021 at 1:48 AM SATYANARAYANA NARLAPURAM
> > > <[email protected]> wrote:
> > > >
> > > >> 3) Instead of the subscriber pulling the slot info, why can't the
> > > >> publisher (via the walsender or a new bg worker maybe?) push the
> > > >> latest slot info? I'm not sure we want to add more functionality to
> > > >> the walsender, if yes, isn't it going to be much simpler?
> > > >
> > > > Standby pulling the information or at least making a first attempt to connect to the  primary is a better design as primary doesn't need to spend its cycles repeatedly connecting to an unreachable standby. In fact, primary wouldn't even need to know the followers, for example followers / log shipping standbys
> > >
> > > My idea was to let the existing walsender from the primary/publisher
> > > to send the slot info (both logical and physical replication slots) to
> > > the standby/subscriber, probably by piggybacking the slot info with
> > > the WAL currently it sends. Having said that, I don't know the
> > > feasibility of it. Anyways, I'm not in favour of having a new bg
> > > worker to just ship the slot info. The standby/subscriber, while
> > > making connection to primary/publisher, can choose to get the
> > > replication slot info.
> >
> > I think it is possible that the standby is restoring the WAL directly
> > from the archive location and there might not be any wal sender at
> > time. So I think the idea of standby pulling the WAL looks better to
> > me.
>
> My point was that why can't we let the walreceiver (of course users
> can configure it on the standby/subscriber) to choose whether or not
> to receive the replication (both physical and logical) slot info from
> the primary/publisher and if yes, the walsender(on the
> primary/publisher) sending it probably as a new WAL record or just
> piggybacking the replication slot info with any of the existing WAL
> records.

Okay, I thought your point was that the primary pushing is better over
standby pulling the slot info, but now it seems that you also agree
that standby pulling is better right?  Now it appears your point is
about whether we will use the same connection for pulling the slot
information which we are using for streaming the data or any other
connection?  I mean in this patch also we are creating a replication
connection and pulling the slot information over there, just point is
we are starting a separate worker for pulling the slot information,
and I think that approach is better as this will not impact the
performance of the other replication connection which we are using for
communicating the data.

-- 
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-11-29 12:28  Bharath Rupireddy <[email protected]>
  parent: Dilip Kumar <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Bharath Rupireddy @ 2021-11-29 12:28 UTC (permalink / raw)
  To: Dilip Kumar <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Peter Eisentraut <[email protected]>; pgsql-hackers

On Mon, Nov 29, 2021 at 1:10 PM Dilip Kumar <[email protected]> wrote:
>
> On Mon, Nov 29, 2021 at 12:19 PM Bharath Rupireddy
> <[email protected]> wrote:
> >
> > On Mon, Nov 29, 2021 at 11:14 AM Dilip Kumar <[email protected]> wrote:
> > >
> > > On Mon, Nov 29, 2021 at 9:40 AM Bharath Rupireddy
> > > <[email protected]> wrote:
> > > >
> > > > On Mon, Nov 29, 2021 at 1:48 AM SATYANARAYANA NARLAPURAM
> > > > <[email protected]> wrote:
> > > > >
> > > > >> 3) Instead of the subscriber pulling the slot info, why can't the
> > > > >> publisher (via the walsender or a new bg worker maybe?) push the
> > > > >> latest slot info? I'm not sure we want to add more functionality to
> > > > >> the walsender, if yes, isn't it going to be much simpler?
> > > > >
> > > > > Standby pulling the information or at least making a first attempt to connect to the  primary is a better design as primary doesn't need to spend its cycles repeatedly connecting to an unreachable standby. In fact, primary wouldn't even need to know the followers, for example followers / log shipping standbys
> > > >
> > > > My idea was to let the existing walsender from the primary/publisher
> > > > to send the slot info (both logical and physical replication slots) to
> > > > the standby/subscriber, probably by piggybacking the slot info with
> > > > the WAL currently it sends. Having said that, I don't know the
> > > > feasibility of it. Anyways, I'm not in favour of having a new bg
> > > > worker to just ship the slot info. The standby/subscriber, while
> > > > making connection to primary/publisher, can choose to get the
> > > > replication slot info.
> > >
> > > I think it is possible that the standby is restoring the WAL directly
> > > from the archive location and there might not be any wal sender at
> > > time. So I think the idea of standby pulling the WAL looks better to
> > > me.
> >
> > My point was that why can't we let the walreceiver (of course users
> > can configure it on the standby/subscriber) to choose whether or not
> > to receive the replication (both physical and logical) slot info from
> > the primary/publisher and if yes, the walsender(on the
> > primary/publisher) sending it probably as a new WAL record or just
> > piggybacking the replication slot info with any of the existing WAL
> > records.
>
> Okay, I thought your point was that the primary pushing is better over
> standby pulling the slot info, but now it seems that you also agree
> that standby pulling is better right?  Now it appears your point is
> about whether we will use the same connection for pulling the slot
> information which we are using for streaming the data or any other
> connection?  I mean in this patch also we are creating a replication
> connection and pulling the slot information over there, just point is
> we are starting a separate worker for pulling the slot information,
> and I think that approach is better as this will not impact the
> performance of the other replication connection which we are using for
> communicating the data.

The easiest way to implement this feature so far, is to use a common
bg worker (as opposed to the bg worker proposed originally in this
thread which, IIUC, works for logical replication) running on standby
(in case of streaming replication with physical replication slots) or
subscriber (in case of logical replication with logical replication
slots) for getting both the physical and logical replication slots
info from the primary or publisher. This bg worker requires at least
two GUCs, 1) to enable/disable the worker 2) to define the slot sync
interval (the bg worker gets the slots info after every sync interval
of time).

Thoughts?

Regards,
Bharath Rupireddy.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-12-14 22:12  Peter Eisentraut <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  3 siblings, 1 reply; 119+ messages in thread

From: Peter Eisentraut @ 2021-12-14 22:12 UTC (permalink / raw)
  To: pgsql-hackers

On 31.10.21 11:08, Peter Eisentraut wrote:
> I want to reactivate $subject.  I took Petr Jelinek's patch from [0], 
> rebased it, added a bit of testing.  It basically works, but as 
> mentioned in [0], there are various issues to work out.
> 
> The idea is that the standby runs a background worker to periodically 
> fetch replication slot information from the primary.  On failover, a 
> logical subscriber would then ideally find up-to-date replication slots 
> on the new publisher and can just continue normally.

> So, again, this isn't anywhere near ready, but there is already a lot 
> here to gather feedback about how it works, how it should work, how to 
> configure it, and how it fits into an overall replication and HA 
> architecture.

Here is an updated patch.  The main changes are that I added two 
configuration parameters.  The first, synchronize_slot_names, is set on 
the physical standby to specify which slots to sync from the primary. 
By default, it is empty.  (This also fixes the recovery test failures 
that I had to disable in the previous patch version.)  The second, 
standby_slot_names, is set on the primary.  It holds back logical 
replication until the listed physical standbys have caught up.  That 
way, when failover is necessary, the promoted standby is not behind the 
logical replication consumers.

In principle, this works now, I think.  I haven't made much progress in 
creating more test cases for this; that's something that needs more 
attention.

It's worth pondering what the configuration language for 
standby_slot_names should be.  Right now, it's just a list of slots that 
all need to be caught up.  More complicated setups are conceivable. 
Maybe you have standbys S1 and S2 that are potential failover targets 
for logical replication consumers L1 and L2, and also standbys S3 and S4 
that are potential failover targets for logical replication consumers L3 
and L4.  Viewed like that, this setting could be a replication slot 
setting.  The setting might also have some relationship with 
synchronous_standby_names.  Like, if you have synchronous_standby_names 
set, then that's a pretty good indication that you also want some or all 
of those standbys in standby_slot_names.  (But note that one is slots 
and one is application names.)  So there are a variety of possibilities.
From f1d306bfe3eb657b5d14b0ef3024586083beb4ed Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 14 Dec 2021 22:20:59 +0100
Subject: [PATCH v2] Synchronize logical replication slots from primary to
 standby

Discussion: https://www.postgresql.org/message-id/flat/514f6f2f-6833-4539-39f1-96cd1e011f23%40enterprisedb.com
---
 doc/src/sgml/config.sgml                      |  34 ++
 src/backend/commands/subscriptioncmds.c       |   4 +-
 src/backend/postmaster/bgworker.c             |   3 +
 .../libpqwalreceiver/libpqwalreceiver.c       |  96 ++++
 src/backend/replication/logical/Makefile      |   1 +
 src/backend/replication/logical/launcher.c    | 202 ++++++---
 .../replication/logical/reorderbuffer.c       |  85 ++++
 src/backend/replication/logical/slotsync.c    | 412 ++++++++++++++++++
 src/backend/replication/logical/tablesync.c   |  13 +-
 src/backend/replication/repl_gram.y           |  32 +-
 src/backend/replication/repl_scanner.l        |   1 +
 src/backend/replication/slotfuncs.c           |   2 +-
 src/backend/replication/walsender.c           | 196 ++++++++-
 src/backend/utils/activity/wait_event.c       |   3 +
 src/backend/utils/misc/guc.c                  |  23 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/commands/subscriptioncmds.h       |   3 +
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/replnodes.h                 |   9 +
 src/include/replication/logicalworker.h       |   9 +
 src/include/replication/slot.h                |   4 +-
 src/include/replication/walreceiver.h         |  16 +
 src/include/replication/worker_internal.h     |   8 +-
 src/include/utils/wait_event.h                |   1 +
 src/test/recovery/t/030_slot_sync.pl          |  58 +++
 25 files changed, 1148 insertions(+), 70 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c
 create mode 100644 src/test/recovery/t/030_slot_sync.pl

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index afbb6c35e3..2b2a21a251 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4406,6 +4406,23 @@ <title>Primary Server</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical replication slots that logical replication waits for.
+        If a logical replication connection is meant to switch to a physical
+        standby after the standby is promoted, the physical replication slot
+        for the standby should be listed here.  This ensures that logical
+        replication is not ahead of the physical standby.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
@@ -4794,6 +4811,23 @@ <title>Standby Servers</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-synchronize_slot_names" xreflabel="synchronize_slot_names">
+      <term><varname>synchronize_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>synchronize_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies a list of logical replication slots that a physical standby
+        should synchronize from the primary server.  This is necessary to be
+        able to retarget those logical replication connections to this standby
+        if it gets promoted.  Specify <literal>*</literal> to synchronize all
+        logical replication slots.  The default is empty.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 2b658080fe..7cdea20207 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -737,7 +737,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
 
 				RemoveSubscriptionRel(sub->oid, relid);
 
-				logicalrep_worker_stop(sub->oid, relid);
+				logicalrep_worker_stop(MyDatabaseId, sub->oid, relid);
 
 				/*
 				 * For READY state, we would have already dropped the
@@ -1239,7 +1239,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	{
 		LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc);
 
-		logicalrep_worker_stop(w->subid, w->relid);
+		logicalrep_worker_stop(w->dbid, w->subid, w->relid);
 	}
 	list_free(subworkers);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index c05f500639..818b8a35e9 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -128,6 +128,9 @@ static const struct
 	},
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
+	},
+	{
+		"ReplSlotSyncMain", ReplSlotSyncMain
 	}
 };
 
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index c08e599eef..57489a0d11 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static List *libpqrcv_list_slots(WalReceiverConn *conn, const char *slot_names);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -89,6 +91,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	libpqrcv_get_conninfo,
 	libpqrcv_get_senderinfo,
 	libpqrcv_identify_system,
+	libpqrcv_list_slots,
 	libpqrcv_server_version,
 	libpqrcv_readtimelinehistoryfile,
 	libpqrcv_startstreaming,
@@ -397,6 +400,99 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get list of slots from primary.
+ */
+static List *
+libpqrcv_list_slots(WalReceiverConn *conn, const char *slot_names)
+{
+	PGresult   *res;
+	List	   *slotlist = NIL;
+	int			ntuples;
+	StringInfoData s;
+	WalRecvReplicationSlotData *slot_data;
+
+	initStringInfo(&s);
+	appendStringInfoString(&s, "LIST_SLOTS");
+
+	if (strcmp(slot_names, "") != 0 && strcmp(slot_names, "*") != 0)
+	{
+		char	   *rawname;
+		List	   *namelist;
+		ListCell   *lc;
+
+		appendStringInfoChar(&s, ' ');
+		rawname = pstrdup(slot_names);
+		SplitIdentifierString(rawname, ',', &namelist);
+		foreach (lc, namelist)
+		{
+			if (lc != list_head(namelist))
+				appendStringInfoChar(&s, ',');
+			appendStringInfo(&s, "%s",
+							 quote_identifier(lfirst(lc)));
+		}
+	}
+
+	res = libpqrcv_PQexec(conn->streamConn, s.data);
+	pfree(s.data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("could not receive list of slots the primary server: %s",
+						pchomp(PQerrorMessage(conn->streamConn)))));
+	}
+	if (PQnfields(res) < 10)
+	{
+		int			nfields = PQnfields(res);
+
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("invalid response from primary server"),
+				 errdetail("Could not get list of slots: got %d fields, expected %d or more fields.",
+						   nfields, 10)));
+	}
+
+	ntuples = PQntuples(res);
+	for (int i = 0; i < ntuples; i++)
+	{
+		char	   *slot_type;
+
+		slot_data = palloc0(sizeof(WalRecvReplicationSlotData));
+		namestrcpy(&slot_data->name, PQgetvalue(res, i, 0));
+		if (!PQgetisnull(res, i, 1))
+			namestrcpy(&slot_data->plugin, PQgetvalue(res, i, 1));
+		slot_type = PQgetvalue(res, i, 2);
+		if (!PQgetisnull(res, i, 3))
+			slot_data->database = atooid(PQgetvalue(res, i, 3));
+		if (strcmp(slot_type, "physical") == 0)
+		{
+			if (OidIsValid(slot_data->database))
+				elog(ERROR, "unexpected physical replication slot with database set");
+		}
+		if (pg_strtoint32(PQgetvalue(res, i, 5)) == 1)
+			slot_data->persistency = RS_TEMPORARY;
+		else
+			slot_data->persistency = RS_PERSISTENT;
+		if (!PQgetisnull(res, i, 6))
+			slot_data->xmin = atooid(PQgetvalue(res, i, 6));
+		if (!PQgetisnull(res, i, 7))
+			slot_data->catalog_xmin = atooid(PQgetvalue(res, i, 7));
+		if (!PQgetisnull(res, i, 8))
+			slot_data->restart_lsn = pg_strtouint64(PQgetvalue(res, i, 8),
+													NULL, 10);
+		if (!PQgetisnull(res, i, 9))
+			slot_data->confirmed_flush = pg_strtouint64(PQgetvalue(res, i, 9),
+														NULL, 10);
+
+		slotlist = lappend(slotlist, slot_data);
+	}
+
+	PQclear(res);
+
+	return slotlist;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index c4e2fdeb71..bc3f23b5a2 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -24,6 +24,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 3fb4caa803..207ef9bc8b 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -22,6 +22,7 @@
 #include "access/htup_details.h"
 #include "access/tableam.h"
 #include "access/xact.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 #include "funcapi.h"
@@ -212,7 +213,7 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker,
  * subscription id and relid.
  */
 LogicalRepWorker *
-logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
+logicalrep_worker_find(Oid dbid, Oid subid, Oid relid, bool only_running)
 {
 	int			i;
 	LogicalRepWorker *res = NULL;
@@ -224,8 +225,8 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
-		if (w->in_use && w->subid == subid && w->relid == relid &&
-			(!only_running || w->proc))
+		if (w->in_use && w->dbid == dbid && w->subid == subid &&
+			w->relid == relid && (!only_running || w->proc))
 		{
 			res = w;
 			break;
@@ -275,9 +276,13 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	int			nsyncworkers;
 	TimestampTz now;
 
-	ereport(DEBUG1,
-			(errmsg_internal("starting logical replication worker for subscription \"%s\"",
-							 subname)));
+	if (OidIsValid(subid))
+		ereport(DEBUG1,
+				(errmsg_internal("starting logical replication worker for subscription \"%s\"",
+								 subname)));
+	else
+		ereport(DEBUG1,
+				(errmsg_internal("starting replication slot synchronization worker")));
 
 	/* Report this after the initial starting message for consistency. */
 	if (max_replication_slots == 0)
@@ -314,7 +319,9 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * reason we do this is because if some worker failed to start up and its
 	 * parent has crashed while waiting, the in_use state was never cleared.
 	 */
-	if (worker == NULL || nsyncworkers >= max_sync_workers_per_subscription)
+	if (worker == NULL ||
+		(OidIsValid(relid) &&
+		 nsyncworkers >= max_sync_workers_per_subscription))
 	{
 		bool		did_cleanup = false;
 
@@ -348,7 +355,7 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * silently as we might get here because of an otherwise harmless race
 	 * condition.
 	 */
-	if (nsyncworkers >= max_sync_workers_per_subscription)
+	if (OidIsValid(relid) && nsyncworkers >= max_sync_workers_per_subscription)
 	{
 		LWLockRelease(LogicalRepWorkerLock);
 		return;
@@ -395,15 +402,22 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	if (OidIsValid(subid))
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	else
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncMain");
 	if (OidIsValid(relid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u sync %u", subid, relid);
-	else
+	else if (OidIsValid(subid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u", subid);
+	else
+		snprintf(bgw.bgw_name, BGW_MAXLEN,
+				 "replication slot synchronization worker");
+
 	snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication worker");
 
 	bgw.bgw_restart_time = BGW_NEVER_RESTART;
@@ -434,14 +448,14 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
  * it detaches from the slot.
  */
 void
-logicalrep_worker_stop(Oid subid, Oid relid)
+logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 	uint16		generation;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, false);
+	worker = logicalrep_worker_find(dbid, subid, relid, false);
 
 	/* No worker, nothing to do. */
 	if (!worker)
@@ -531,13 +545,13 @@ logicalrep_worker_stop(Oid subid, Oid relid)
  * Wake up (using latch) any logical replication worker for specified sub/rel.
  */
 void
-logicalrep_worker_wakeup(Oid subid, Oid relid)
+logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, true);
+	worker = logicalrep_worker_find(dbid, subid, relid, true);
 
 	if (worker)
 		logicalrep_worker_wakeup_ptr(worker);
@@ -714,7 +728,7 @@ ApplyLauncherRegister(void)
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
 	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyLauncherMain");
 	snprintf(bgw.bgw_name, BGW_MAXLEN,
@@ -795,6 +809,116 @@ ApplyLauncherWakeup(void)
 		kill(LogicalRepCtx->launcher_pid, SIGUSR1);
 }
 
+static void
+ApplyLauncherStartSlotSync(TimestampTz *last_start_time, long *wait_time)
+{
+	WalReceiverConn *wrconn;
+	TimestampTz now;
+	char	   *err;
+	List	   *slots;
+	ListCell   *lc;
+	MemoryContext tmpctx;
+	MemoryContext oldctx;
+
+	if (strcmp(synchronize_slot_names, "") == 0)
+		return;
+
+	wrconn = walrcv_connect(PrimaryConnInfo, false,
+							"Logical Replication Launcher", &err);
+	if (!wrconn)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	/* Use temporary context for the slot list and worker info. */
+	tmpctx = AllocSetContextCreate(TopMemoryContext,
+								   "Logical Replication Launcher slot sync ctx",
+								   ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(tmpctx);
+
+	slots = walrcv_list_slots(wrconn, synchronize_slot_names);
+
+	now = GetCurrentTimestamp();
+
+	foreach(lc, slots)
+	{
+		WalRecvReplicationSlotData *slot_data = lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!OidIsValid(slot_data->database))
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(slot_data->database, InvalidOid,
+								   InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(slot_data->database, InvalidOid, NULL,
+									 BOOTSTRAP_SUPERUSERID, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(tmpctx);
+
+	walrcv_disconnect(wrconn);
+}
+
+static void
+ApplyLauncherStartSubs(TimestampTz *last_start_time, long *wait_time)
+{
+	TimestampTz now;
+	List	   *sublist;
+	ListCell   *lc;
+	MemoryContext subctx;
+	MemoryContext oldctx;
+
+	now = GetCurrentTimestamp();
+
+	/* Use temporary context for the database list and worker info. */
+	subctx = AllocSetContextCreate(TopMemoryContext,
+								   "Logical Replication Launcher sublist",
+								   ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(subctx);
+
+	/* search for subscriptions to start or stop. */
+	sublist = get_subscription_list();
+
+	/* Start the missing workers for enabled subscriptions. */
+	foreach(lc, sublist)
+	{
+		Subscription *sub = (Subscription *) lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!sub->enabled)
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(sub->dbid, sub->oid, InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
+									 sub->owner, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(subctx);
+}
+
 /*
  * Main loop for the apply launcher process.
  */
@@ -822,14 +946,12 @@ ApplyLauncherMain(Datum main_arg)
 	 */
 	BackgroundWorkerInitializeConnection(NULL, NULL, 0);
 
+	load_file("libpqwalreceiver", false);
+
 	/* Enter main loop */
 	for (;;)
 	{
 		int			rc;
-		List	   *sublist;
-		ListCell   *lc;
-		MemoryContext subctx;
-		MemoryContext oldctx;
 		TimestampTz now;
 		long		wait_time = DEFAULT_NAPTIME_PER_CYCLE;
 
@@ -841,42 +963,10 @@ ApplyLauncherMain(Datum main_arg)
 		if (TimestampDifferenceExceeds(last_start_time, now,
 									   wal_retrieve_retry_interval))
 		{
-			/* Use temporary context for the database list and worker info. */
-			subctx = AllocSetContextCreate(TopMemoryContext,
-										   "Logical Replication Launcher sublist",
-										   ALLOCSET_DEFAULT_SIZES);
-			oldctx = MemoryContextSwitchTo(subctx);
-
-			/* search for subscriptions to start or stop. */
-			sublist = get_subscription_list();
-
-			/* Start the missing workers for enabled subscriptions. */
-			foreach(lc, sublist)
-			{
-				Subscription *sub = (Subscription *) lfirst(lc);
-				LogicalRepWorker *w;
-
-				if (!sub->enabled)
-					continue;
-
-				LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-				w = logicalrep_worker_find(sub->oid, InvalidOid, false);
-				LWLockRelease(LogicalRepWorkerLock);
-
-				if (w == NULL)
-				{
-					last_start_time = now;
-					wait_time = wal_retrieve_retry_interval;
-
-					logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
-											 sub->owner, InvalidOid);
-				}
-			}
-
-			/* Switch back to original memory context. */
-			MemoryContextSwitchTo(oldctx);
-			/* Clean the temporary memory. */
-			MemoryContextDelete(subctx);
+			if (!RecoveryInProgress())
+				ApplyLauncherStartSubs(&last_start_time, &wait_time);
+			else
+				ApplyLauncherStartSlotSync(&last_start_time, &wait_time);
 		}
 		else
 		{
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 7aa5647a2c..f0b3b9ad87 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -95,11 +95,13 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/reorderbuffer.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"	/* just for SnapBuildSnapDecRefcount */
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
+#include "storage/ipc.h"
 #include "storage/sinval.h"
 #include "utils/builtins.h"
 #include "utils/combocid.h"
@@ -107,6 +109,7 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/relfilenodemap.h"
+#include "utils/varlena.h"
 
 
 /* entry for a hash table we use to map from xid to our transaction state */
@@ -2006,6 +2009,85 @@ ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	}
 }
 
+static void
+wait_for_standby_confirmation(XLogRecPtr commit_lsn)
+{
+	char	   *rawname;
+	List	   *namelist;
+	ListCell   *lc;
+	XLogRecPtr	flush_pos = InvalidXLogRecPtr;
+
+	if (strcmp(standby_slot_names, "") == 0)
+		return;
+
+	rawname = pstrdup(standby_slot_names);
+	SplitIdentifierString(rawname, ',', &namelist);
+
+	while (true)
+	{
+		int			wait_slots_remaining;
+		XLogRecPtr	oldest_flush_pos = InvalidXLogRecPtr;
+		int			rc;
+
+		wait_slots_remaining = list_length(namelist);
+
+		LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+		for (int i = 0; i < max_replication_slots; i++)
+		{
+			ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+			bool		inlist;
+
+			if (!s->in_use)
+				continue;
+
+			inlist = false;
+			foreach (lc, namelist)
+			{
+				char *name = lfirst(lc);
+				if (strcmp(name, NameStr(s->data.name)) == 0)
+				{
+					inlist = true;
+					break;
+				}
+			}
+			if (!inlist)
+				continue;
+
+			SpinLockAcquire(&s->mutex);
+
+			if (s->data.database == InvalidOid)
+				/* Physical slots advance restart_lsn on flush and ignore confirmed_flush_lsn */
+				flush_pos = s->data.restart_lsn;
+			else
+				/* For logical slots we must wait for commit and flush */
+				flush_pos = s->data.confirmed_flush;
+
+			SpinLockRelease(&s->mutex);
+
+			/* We want to find out the min(flush pos) over all named slots */
+			if (oldest_flush_pos == InvalidXLogRecPtr
+				|| oldest_flush_pos > flush_pos)
+				oldest_flush_pos = flush_pos;
+
+			if (flush_pos >= commit_lsn && wait_slots_remaining > 0)
+				wait_slots_remaining --;
+		}
+		LWLockRelease(ReplicationSlotControlLock);
+
+		if (wait_slots_remaining == 0)
+			return;
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   1000L, PG_WAIT_EXTENSION);
+
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+
+		CHECK_FOR_INTERRUPTS();
+	}
+}
+
 /*
  * Helper function for ReorderBufferReplay and ReorderBufferStreamTXN.
  *
@@ -2434,6 +2516,9 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			 * Call either PREPARE (for two-phase transactions) or COMMIT (for
 			 * regular ones).
 			 */
+
+			wait_for_standby_confirmation(commit_lsn);
+
 			if (rbtxn_prepared(txn))
 				rb->prepare(rb, txn, commit_lsn);
 			else
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..654ac154ea
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,412 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby from primary
+ *
+ * Copyright (c) 2016-2018, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "utils/builtins.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+char	   *synchronize_slot_names;
+char	   *standby_slot_names;
+
+/*
+ * Wait for remote slot to pass localy reserved position.
+ */
+static void
+wait_for_primary_slot_catchup(WalReceiverConn *wrconn, char *slot_name,
+							  XLogRecPtr min_lsn)
+{
+	WalRcvExecResult *res;
+	TupleTableSlot *slot;
+	Oid			slotRow[1] = {LSNOID};
+	StringInfoData cmd;
+	bool		isnull;
+	XLogRecPtr	restart_lsn;
+
+	for (;;)
+	{
+		int			rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		initStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "SELECT restart_lsn"
+						 "  FROM pg_catalog.pg_replication_slots"
+						 " WHERE slot_name = %s",
+						 quote_literal_cstr(slot_name));
+		res = walrcv_exec(wrconn, cmd.data, 1, slotRow);
+
+		if (res->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errmsg("could not fetch slot info for slot \"%s\" from primary: %s",
+							slot_name, res->err)));
+
+		slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+		if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+			ereport(ERROR,
+					(errmsg("slot \"%s\" disapeared from provider",
+							slot_name)));
+
+		restart_lsn = DatumGetLSN(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		ExecClearTuple(slot);
+		walrcv_clear_result(res);
+
+		if (restart_lsn >= min_lsn)
+			break;
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This optionally creates new slot if there is no existing one.
+ */
+static void
+synchronize_one_slot(WalReceiverConn *wrconn, char *slot_name, char *database,
+					 char *plugin_name, XLogRecPtr target_lsn)
+{
+	bool		found = false;
+	XLogRecPtr	endlsn;
+
+	/* Search for the named slot and mark it active if we find it. */
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (!s->in_use)
+			continue;
+
+		if (strcmp(NameStr(s->data.name), slot_name) == 0)
+		{
+			found = true;
+			break;
+		}
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	StartTransactionCommand();
+
+	/* Already existing slot, acquire */
+	if (found)
+	{
+		ReplicationSlotAcquire(slot_name, true);
+
+		if (target_lsn < MyReplicationSlot->data.confirmed_flush)
+		{
+			elog(DEBUG1,
+				 "not synchronizing slot %s; synchronization would move it backward",
+				 slot_name);
+
+			ReplicationSlotRelease();
+			CommitTransactionCommand();
+			return;
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+		ReplicationSlot *slot;
+
+		ReplicationSlotCreate(slot_name, true, RS_EPHEMERAL, false);
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(database, false);
+		namestrcpy(&slot->data.plugin, plugin_name);
+		SpinLockRelease(&slot->mutex);
+
+		ReplicationSlotReserveWal();
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		if (target_lsn < MyReplicationSlot->data.restart_lsn)
+		{
+			ereport(LOG,
+					errmsg("waiting for remote slot \"%s\" LSN (%X/%X) to pass local slot LSN (%X/%X)",
+						   slot_name,
+						   LSN_FORMAT_ARGS(target_lsn), LSN_FORMAT_ARGS(MyReplicationSlot->data.restart_lsn)));
+
+			wait_for_primary_slot_catchup(wrconn, slot_name,
+										  MyReplicationSlot->data.restart_lsn);
+		}
+
+		ReplicationSlotPersist();
+	}
+
+	endlsn = pg_logical_replication_slot_advance(target_lsn);
+
+	elog(DEBUG3, "synchronized slot %s to lsn (%X/%X)",
+		 slot_name, LSN_FORMAT_ARGS(endlsn));
+
+	ReplicationSlotRelease();
+	CommitTransactionCommand();
+}
+
+static void
+synchronize_slots(void)
+{
+	WalRcvExecResult *res;
+	WalReceiverConn *wrconn = NULL;
+	TupleTableSlot *slot;
+	Oid			slotRow[3] = {TEXTOID, TEXTOID, LSNOID};
+	StringInfoData s;
+	char	   *database;
+	char	   *err;
+	MemoryContext oldctx = CurrentMemoryContext;
+
+	if (!WalRcv)
+		return;
+
+	/* syscache access needs a transaction env. */
+	StartTransactionCommand();
+	/* make dbname live outside TX context */
+	MemoryContextSwitchTo(oldctx);
+
+	database = get_database_name(MyDatabaseId);
+	initStringInfo(&s);
+	appendStringInfo(&s, "%s dbname=%s", PrimaryConnInfo, database);
+	wrconn = walrcv_connect(s.data, true, "slot_sync", &err);
+
+	if (wrconn == NULL)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	resetStringInfo(&s);
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn"
+					 "  FROM pg_catalog.pg_replication_slots"
+					 " WHERE database = %s",
+					 quote_literal_cstr(database));
+	if (strcmp(synchronize_slot_names, "") != 0 && strcmp(synchronize_slot_names, "*") != 0)
+	{
+		char	   *rawname;
+		List	   *namelist;
+		ListCell   *lc;
+
+		rawname = pstrdup(synchronize_slot_names);
+		SplitIdentifierString(rawname, ',', &namelist);
+
+		appendStringInfoString(&s, " AND slot_name IN (");
+		foreach (lc, namelist)
+		{
+			if (lc != list_head(namelist))
+				appendStringInfoChar(&s, ',');
+			appendStringInfo(&s, "%s",
+							 quote_literal_cstr(lfirst(lc)));
+		}
+		appendStringInfoChar(&s, ')');
+	}
+
+	res = walrcv_exec(wrconn, s.data, 3, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				(errmsg("could not fetch slot info from primary: %s",
+						res->err)));
+
+	CommitTransactionCommand();
+	/* CommitTransactionCommand switches to TopMemoryContext */
+	MemoryContextSwitchTo(oldctx);
+
+	slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+	{
+		char	   *slot_name;
+		char	   *plugin_name;
+		XLogRecPtr	confirmed_flush_lsn;
+		bool		isnull;
+
+		slot_name = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		plugin_name = TextDatumGetCString(slot_getattr(slot, 2, &isnull));
+		Assert(!isnull);
+
+		confirmed_flush_lsn = DatumGetLSN(slot_getattr(slot, 3, &isnull));
+		Assert(!isnull);
+
+		synchronize_one_slot(wrconn, slot_name, database, plugin_name,
+							 confirmed_flush_lsn);
+
+		ExecClearTuple(slot);
+	}
+
+	walrcv_clear_result(res);
+	pfree(database);
+
+	walrcv_disconnect(wrconn);
+}
+
+/*
+ * The main loop of our worker process.
+ */
+void
+ReplSlotSyncMain(Datum main_arg)
+{
+	int			worker_slot = DatumGetInt32(main_arg);
+
+	/* Attach to slot */
+	logicalrep_worker_attach(worker_slot);
+
+	/* Establish signal handlers. */
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	/* Connect to our database. */
+	BackgroundWorkerInitializeConnectionByOid(MyLogicalRepWorker->dbid,
+											  MyLogicalRepWorker->userid,
+											  0);
+
+	StartTransactionCommand();
+	ereport(LOG,
+			(errmsg("replication slot synchronization worker for database \"%s\" has started",
+					get_database_name(MyLogicalRepWorker->dbid))));
+	CommitTransactionCommand();
+
+	/* Main wait loop. */
+	for (;;)
+	{
+		int			rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		if (!RecoveryInProgress())
+			return;
+
+		if (strcmp(synchronize_slot_names, "") == 0)
+			return;
+
+		synchronize_slots();
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
+
+/*
+ * Routines for handling the GUC variable(s)
+ */
+
+bool
+check_synchronize_slot_names(char **newval, void **extra, GucSource source)
+{
+	/* Special handling for "*" which means all. */
+	if (strcmp(*newval, "*") == 0)
+	{
+		return true;
+	}
+	else
+	{
+		char	   *rawname;
+		List	   *namelist;
+		ListCell   *lc;
+
+		/* Need a modifiable copy of string */
+		rawname = pstrdup(*newval);
+
+		/* Parse string into list of identifiers */
+		if (!SplitIdentifierString(rawname, ',', &namelist))
+		{
+			/* syntax error in name list */
+			GUC_check_errdetail("List syntax is invalid.");
+			pfree(rawname);
+			list_free(namelist);
+			return false;
+		}
+
+		foreach(lc, namelist)
+		{
+			char	   *curname = (char *) lfirst(lc);
+
+			ReplicationSlotValidateName(curname, ERROR);
+		}
+
+		pfree(rawname);
+		list_free(namelist);
+	}
+
+	return true;
+}
+
+
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	char	   *rawname;
+	List	   *namelist;
+	ListCell   *lc;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Parse string into list of identifiers */
+	if (!SplitIdentifierString(rawname, ',', &namelist))
+	{
+		/* syntax error in name list */
+		GUC_check_errdetail("List syntax is invalid.");
+		pfree(rawname);
+		list_free(namelist);
+		return false;
+	}
+
+	foreach(lc, namelist)
+	{
+		char	   *curname = (char *) lfirst(lc);
+
+		ReplicationSlotValidateName(curname, ERROR);
+	}
+
+	pfree(rawname);
+	list_free(namelist);
+
+	return true;
+}
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f07983a43c..0e0593f716 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -100,6 +100,7 @@
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_type.h"
 #include "commands/copy.h"
+#include "commands/subscriptioncmds.h"
 #include "miscadmin.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -151,7 +152,8 @@ finish_sync_worker(void)
 	CommitTransactionCommand();
 
 	/* Find the main apply worker and signal it. */
-	logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid);
+	logicalrep_worker_wakeup(MyLogicalRepWorker->dbid,
+							 MyLogicalRepWorker->subid, InvalidOid);
 
 	/* Stop gracefully */
 	proc_exit(0);
@@ -191,7 +193,8 @@ wait_for_relation_state_change(Oid relid, char expected_state)
 
 		/* Check if the sync worker is still running and bail if not. */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid, relid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid, relid,
 										false);
 		LWLockRelease(LogicalRepWorkerLock);
 		if (!worker)
@@ -238,7 +241,8 @@ wait_for_worker_state_change(char expected_state)
 		 * waiting.
 		 */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid,
 										InvalidOid, false);
 		if (worker && worker->proc)
 			logicalrep_worker_wakeup_ptr(worker);
@@ -484,7 +488,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn)
 			 */
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-			syncworker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+			syncworker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+												MyLogicalRepWorker->subid,
 												rstate->relid, false);
 
 			if (syncworker)
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index dcb1108579..902841efe6 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -91,11 +91,12 @@ static SQLCmd *make_sqlcmd(void);
 %token K_USE_SNAPSHOT
 %token K_MANIFEST
 %token K_MANIFEST_CHECKSUMS
+%token K_LIST_SLOTS
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
 				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show sql_cmd
+				read_replication_slot timeline_history show sql_cmd list_slots
 %type <list>	base_backup_legacy_opt_list generic_option_list
 %type <defelt>	base_backup_legacy_opt generic_option
 %type <uintval>	opt_timeline
@@ -106,6 +107,7 @@ static SQLCmd *make_sqlcmd(void);
 %type <boolval>	opt_temporary
 %type <list>	create_slot_options create_slot_legacy_opt_list
 %type <defelt>	create_slot_legacy_opt
+%type <list>	slot_name_list slot_name_list_opt
 
 %%
 
@@ -129,6 +131,7 @@ command:
 			| read_replication_slot
 			| timeline_history
 			| show
+			| list_slots
 			| sql_cmd
 			;
 
@@ -142,6 +145,33 @@ identify_system:
 				}
 			;
 
+slot_name_list:
+			IDENT
+				{
+					$$ = list_make1($1);
+				}
+			| slot_name_list ',' IDENT
+				{
+					$$ = lappend($1, $3);
+				}
+
+slot_name_list_opt:
+			slot_name_list			{ $$ = $1; }
+			| /* EMPTY */			{ $$ = NIL; }
+		;
+
+/*
+ * LIST_SLOTS
+ */
+list_slots:
+			K_LIST_SLOTS slot_name_list_opt
+				{
+					ListSlotsCmd *cmd = makeNode(ListSlotsCmd);
+					cmd->slot_names = $2;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * READ_REPLICATION_SLOT %s
  */
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 1b599c255e..9ee638355d 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -85,6 +85,7 @@ identifier		{ident_start}{ident_cont}*
 BASE_BACKUP			{ return K_BASE_BACKUP; }
 FAST			{ return K_FAST; }
 IDENTIFY_SYSTEM		{ return K_IDENTIFY_SYSTEM; }
+LIST_SLOTS		{ return K_LIST_SLOTS; }
 READ_REPLICATION_SLOT	{ return K_READ_REPLICATION_SLOT; }
 SHOW		{ return K_SHOW; }
 LABEL			{ return K_LABEL; }
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index d11daeb1fc..e93fea55ad 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -484,7 +484,7 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
  * WAL and removal of old catalog tuples.  As decoding is done in fast_forward
  * mode, no changes are generated anyway.
  */
-static XLogRecPtr
+XLogRecPtr
 pg_logical_replication_slot_advance(XLogRecPtr moveto)
 {
 	LogicalDecodingContext *ctx;
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 84915ed95b..2fce290ed6 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -456,6 +456,194 @@ IdentifySystem(void)
 	end_tup_output(tstate);
 }
 
+static int
+pg_qsort_namecmp(const void *a, const void *b)
+{
+	return strncmp(NameStr(*(Name) a), NameStr(*(Name) b), NAMEDATALEN);
+}
+
+/*
+ * Handle the LIST_SLOTS command.
+ */
+static void
+ListSlots(ListSlotsCmd *cmd)
+{
+	DestReceiver *dest;
+	TupOutputState *tstate;
+	TupleDesc	tupdesc;
+	NameData   *slot_names;
+	int			numslot_names;
+
+	numslot_names = list_length(cmd->slot_names);
+	if (numslot_names)
+	{
+		ListCell   *lc;
+		int			i = 0;
+
+		slot_names = palloc(numslot_names * sizeof(NameData));
+		foreach(lc, cmd->slot_names)
+		{
+			char	   *slot_name = lfirst(lc);
+
+			ReplicationSlotValidateName(slot_name, ERROR);
+			namestrcpy(&slot_names[i++], slot_name);
+		}
+
+		qsort(slot_names, numslot_names, sizeof(NameData), pg_qsort_namecmp);
+	}
+
+	dest = CreateDestReceiver(DestRemoteSimple);
+
+	/* need a tuple descriptor representing four columns */
+	tupdesc = CreateTemplateTupleDesc(10);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "slot_name",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "plugin",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "slot_type",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 4, "datoid",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 5, "database",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 6, "temporary",
+							  INT4OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 7, "xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 8, "catalog_xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 9, "restart_lsn",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 10, "confirmed_flush",
+							  TEXTOID, -1, 0);
+
+	/* prepare for projection of tuples */
+	tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual);
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (int slotno = 0; slotno < max_replication_slots; slotno++)
+	{
+		ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno];
+		char		restart_lsn_str[MAXFNAMELEN];
+		char		confirmed_flush_lsn_str[MAXFNAMELEN];
+		Datum		values[10];
+		bool		nulls[10];
+
+		ReplicationSlotPersistency persistency;
+		TransactionId xmin;
+		TransactionId catalog_xmin;
+		XLogRecPtr	restart_lsn;
+		XLogRecPtr	confirmed_flush_lsn;
+		Oid			datoid;
+		NameData	slot_name;
+		NameData	plugin;
+		int			i;
+		int64		tmpbigint;
+
+		if (!slot->in_use)
+			continue;
+
+		SpinLockAcquire(&slot->mutex);
+
+		xmin = slot->data.xmin;
+		catalog_xmin = slot->data.catalog_xmin;
+		datoid = slot->data.database;
+		restart_lsn = slot->data.restart_lsn;
+		confirmed_flush_lsn = slot->data.confirmed_flush;
+		namestrcpy(&slot_name, NameStr(slot->data.name));
+		namestrcpy(&plugin, NameStr(slot->data.plugin));
+		persistency = slot->data.persistency;
+
+		SpinLockRelease(&slot->mutex);
+
+		if (numslot_names &&
+			!bsearch((void *) &slot_name, (void *) slot_names,
+					 numslot_names, sizeof(NameData), pg_qsort_namecmp))
+			continue;
+
+		memset(nulls, 0, sizeof(nulls));
+
+		i = 0;
+		values[i++] = CStringGetTextDatum(NameStr(slot_name));
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+			values[i++] = CStringGetTextDatum(NameStr(plugin));
+
+		if (datoid == InvalidOid)
+			values[i++] = CStringGetTextDatum("physical");
+		else
+			values[i++] = CStringGetTextDatum("logical");
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			tmpbigint = datoid;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			MemoryContext cur = CurrentMemoryContext;
+
+			/* syscache access needs a transaction env. */
+			StartTransactionCommand();
+			/* make dbname live outside TX context */
+			MemoryContextSwitchTo(cur);
+			values[i++] = CStringGetTextDatum(get_database_name(datoid));
+			CommitTransactionCommand();
+			/* CommitTransactionCommand switches to TopMemoryContext */
+			MemoryContextSwitchTo(cur);
+		}
+
+		values[i++] = Int32GetDatum(persistency == RS_TEMPORARY ? 1 : 0);
+
+		if (xmin != InvalidTransactionId)
+		{
+			tmpbigint = xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (catalog_xmin != InvalidTransactionId)
+		{
+			tmpbigint = catalog_xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (restart_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(restart_lsn_str, sizeof(restart_lsn_str), "%X/%X",
+					 LSN_FORMAT_ARGS(restart_lsn));
+			values[i++] = CStringGetTextDatum(restart_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		if (confirmed_flush_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(confirmed_flush_lsn_str, sizeof(confirmed_flush_lsn_str),
+					 "%X/%X", LSN_FORMAT_ARGS(confirmed_flush_lsn));
+			values[i++] = CStringGetTextDatum(confirmed_flush_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		/* send it to dest */
+		do_tup_output(tstate, values, nulls);
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	end_tup_output(tstate);
+}
+
 /* Handle READ_REPLICATION_SLOT command */
 static void
 ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
@@ -554,7 +742,6 @@ ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
 	end_tup_output(tstate);
 }
 
-
 /*
  * Handle TIMELINE_HISTORY command.
  */
@@ -1749,6 +1936,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_ListSlotsCmd:
+			cmdtag = "LIST_SLOTS";
+			set_ps_display(cmdtag);
+			ListSlots((ListSlotsCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4d53f040e8..6922353c94 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -230,6 +230,9 @@ pgstat_get_wait_activity(WaitEventActivity w)
 		case WAIT_EVENT_LOGICAL_LAUNCHER_MAIN:
 			event_name = "LogicalLauncherMain";
 			break;
+		case WAIT_EVENT_REPL_SLOT_SYNC_MAIN:
+			event_name = "ReplSlotSyncMain";
+			break;
 		case WAIT_EVENT_PGSTAT_MAIN:
 			event_name = "PgStatMain";
 			break;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index f736e8d872..ff1eab0207 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -75,6 +75,7 @@
 #include "postmaster/syslogger.h"
 #include "postmaster/walwriter.h"
 #include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
 #include "replication/reorderbuffer.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
@@ -4638,6 +4639,28 @@ static struct config_string ConfigureNamesString[] =
 		check_backtrace_functions, assign_backtrace_functions, NULL
 	},
 
+	{
+		{"synchronize_slot_names", PGC_SIGHUP, REPLICATION_STANDBY,
+			gettext_noop("Sets the names of replication slots which to synchronize from primary to standby."),
+			gettext_noop("Value of \"*\" means all."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&synchronize_slot_names,
+		"",
+		check_synchronize_slot_names, NULL, NULL
+	},
+
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("List of physical slots that must confirm changes before changes are sent to logical replication consumers."),
+			NULL,
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a1acd46b61..e8b5f76125 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -315,6 +315,7 @@
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
 #vacuum_defer_cleanup_age = 0	# number of xacts by which cleanup is delayed
+#standby_slot_names = ''	# physical standby slot names that logical replication waits for
 
 # - Standby Servers -
 
@@ -343,6 +344,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#synchronize_slot_names = ''		# logical replication slots to sync to standby
 
 # - Subscribers -
 
diff --git a/src/include/commands/subscriptioncmds.h b/src/include/commands/subscriptioncmds.h
index aec7e478ab..1cc19e0c99 100644
--- a/src/include/commands/subscriptioncmds.h
+++ b/src/include/commands/subscriptioncmds.h
@@ -17,6 +17,7 @@
 
 #include "catalog/objectaddress.h"
 #include "parser/parse_node.h"
+#include "replication/walreceiver.h"
 
 extern ObjectAddress CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 										bool isTopLevel);
@@ -26,4 +27,6 @@ extern void DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel);
 extern ObjectAddress AlterSubscriptionOwner(const char *name, Oid newOwnerId);
 extern void AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId);
 
+extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
+
 #endif							/* SUBSCRIPTIONCMDS_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 7c657c1241..920a510c4c 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -501,6 +501,7 @@ typedef enum NodeTag
 	T_StartReplicationCmd,
 	T_TimeLineHistoryCmd,
 	T_SQLCmd,
+	T_ListSlotsCmd,
 
 	/*
 	 * TAGS FOR RANDOM OTHER STUFF
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index a746fafc12..3f81409e50 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -33,6 +33,15 @@ typedef struct IdentifySystemCmd
 	NodeTag		type;
 } IdentifySystemCmd;
 
+/* ----------------------
+ *		LIST_SLOTS command
+ * ----------------------
+ */
+typedef struct ListSlotsCmd
+{
+	NodeTag		type;
+	List	   *slot_names;
+} ListSlotsCmd;
 
 /* ----------------------
  *		BASE_BACKUP command
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index 2ad61a001a..f5b5ef07e8 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -12,8 +12,17 @@
 #ifndef LOGICALWORKER_H
 #define LOGICALWORKER_H
 
+#include "utils/guc.h"
+
+extern char *synchronize_slot_names;
+extern char *standby_slot_names;
+
 extern void ApplyWorkerMain(Datum main_arg);
+extern void ReplSlotSyncMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 
+extern bool check_synchronize_slot_names(char **newval, void **extra, GucSource source);
+extern bool check_standby_slot_names(char **newval, void **extra, GucSource source);
+
 #endif							/* LOGICALWORKER_H */
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 53d773ccff..a29e517707 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -216,7 +216,6 @@ extern void ReplicationSlotsDropDBSlots(Oid dboid);
 extern bool InvalidateObsoleteReplicationSlots(XLogSegNo oldestSegno);
 extern ReplicationSlot *SearchNamedReplicationSlot(const char *name, bool need_lock);
 extern void ReplicationSlotNameForTablesync(Oid suboid, Oid relid, char *syncslotname, int szslot);
-extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
 
 extern void StartupReplicationSlots(void);
 extern void CheckPointReplicationSlots(void);
@@ -224,4 +223,7 @@ extern void CheckPointReplicationSlots(void);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern XLogRecPtr pg_logical_replication_slot_advance(XLogRecPtr moveto);
+
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0b607ed777..dbeb447e7a 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -18,6 +18,7 @@
 #include "pgtime.h"
 #include "port/atomics.h"
 #include "replication/logicalproto.h"
+#include "replication/slot.h"
 #include "replication/walsender.h"
 #include "storage/condition_variable.h"
 #include "storage/latch.h"
@@ -187,6 +188,13 @@ typedef struct
 	}			proto;
 } WalRcvStreamOptions;
 
+/*
+ * Slot information receiver from remote.
+ *
+ * Currently this is same as ReplicationSlotPersistentData
+ */
+#define WalRecvReplicationSlotData ReplicationSlotPersistentData
+
 struct WalReceiverConn;
 typedef struct WalReceiverConn WalReceiverConn;
 
@@ -274,6 +282,11 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * TODO
+ */
+typedef List *(*walrcv_list_slots_fn) (WalReceiverConn *conn, const char *slots);
+
 /*
  * walrcv_server_version_fn
  *
@@ -387,6 +400,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_list_slots_fn walrcv_list_slots;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -411,6 +425,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_list_slots(conn, slots) \
+	WalReceiverFunctions->walrcv_list_slots(conn, slots)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 9d29849d80..5de7f80e79 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -58,7 +58,7 @@ typedef struct LogicalRepWorker
 	 * exits.  Under this, separate buffiles would be created for each
 	 * transaction which will be deleted after the transaction is finished.
 	 */
-	FileSet    *stream_fileset;
+	struct FileSet *stream_fileset;
 
 	/* Stats. */
 	XLogRecPtr	last_lsn;
@@ -81,13 +81,13 @@ extern LogicalRepWorker *MyLogicalRepWorker;
 extern bool in_remote_transaction;
 
 extern void logicalrep_worker_attach(int slot);
-extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
+extern LogicalRepWorker *logicalrep_worker_find(Oid dbid, Oid subid, Oid relid,
 												bool only_running);
 extern List *logicalrep_workers_find(Oid subid, bool only_running);
 extern void logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname,
 									 Oid userid, Oid relid);
-extern void logicalrep_worker_stop(Oid subid, Oid relid);
-extern void logicalrep_worker_wakeup(Oid subid, Oid relid);
+extern void logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid);
+extern void logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid);
 extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker);
 
 extern int	logicalrep_sync_worker_count(Oid subid);
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 8785a8e12c..3274eade53 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -42,6 +42,7 @@ typedef enum
 	WAIT_EVENT_CHECKPOINTER_MAIN,
 	WAIT_EVENT_LOGICAL_APPLY_MAIN,
 	WAIT_EVENT_LOGICAL_LAUNCHER_MAIN,
+	WAIT_EVENT_REPL_SLOT_SYNC_MAIN,
 	WAIT_EVENT_PGSTAT_MAIN,
 	WAIT_EVENT_RECOVERY_WAL_STREAM,
 	WAIT_EVENT_SYSLOGGER_MAIN,
diff --git a/src/test/recovery/t/030_slot_sync.pl b/src/test/recovery/t/030_slot_sync.pl
new file mode 100644
index 0000000000..c87e7dc016
--- /dev/null
+++ b/src/test/recovery/t/030_slot_sync.pl
@@ -0,0 +1,58 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 2;
+
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 'logical');
+$node_primary->append_conf('postgresql.conf', "standby_slot_names = 'pslot1'");
+$node_primary->start;
+$node_primary->psql('postgres', q{SELECT pg_create_physical_replication_slot('pslot1');});
+
+$node_primary->backup('backup');
+
+my $node_phys_standby = PostgreSQL::Test::Cluster->new('phys_standby');
+$node_phys_standby->init_from_backup($node_primary, 'backup', has_streaming => 1);
+$node_phys_standby->append_conf('postgresql.conf', "synchronize_slot_names = '*'");
+$node_phys_standby->append_conf('postgresql.conf', "primary_slot_name = 'pslot1'");
+$node_phys_standby->start;
+
+$node_primary->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+$node_primary->safe_psql('postgres', "INSERT INTO t1 VALUES (1), (2), (3)");
+
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$node_subscriber->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+
+$node_primary->safe_psql('postgres', "CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '" . ($node_primary->connstr . ' dbname=postgres') . "' PUBLICATION pub1");
+
+# Wait for initial sync of all subscriptions
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+my $result = $node_primary->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots WHERE slot_type = 'logical'");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on primary');
+
+# FIXME: standby needs restart to pick up new slots
+$node_phys_standby->restart;
+sleep 3;
+
+$result = $node_phys_standby->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on standby');
+
+$node_primary->safe_psql('postgres', "INSERT INTO t1 VALUES (4), (5), (6)");
+$node_primary->wait_for_catchup('sub1');

base-commit: ece8c76192fee0b78509688325631ceabca44ff5
-- 
2.34.1



Attachments:

  [text/plain] v2-0001-Synchronize-logical-replication-slots-from-primar.patch (55.8K, ../../[email protected]/2-v2-0001-Synchronize-logical-replication-slots-from-primar.patch)
  download | inline diff:
From f1d306bfe3eb657b5d14b0ef3024586083beb4ed Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 14 Dec 2021 22:20:59 +0100
Subject: [PATCH v2] Synchronize logical replication slots from primary to
 standby

Discussion: https://www.postgresql.org/message-id/flat/514f6f2f-6833-4539-39f1-96cd1e011f23%40enterprisedb.com
---
 doc/src/sgml/config.sgml                      |  34 ++
 src/backend/commands/subscriptioncmds.c       |   4 +-
 src/backend/postmaster/bgworker.c             |   3 +
 .../libpqwalreceiver/libpqwalreceiver.c       |  96 ++++
 src/backend/replication/logical/Makefile      |   1 +
 src/backend/replication/logical/launcher.c    | 202 ++++++---
 .../replication/logical/reorderbuffer.c       |  85 ++++
 src/backend/replication/logical/slotsync.c    | 412 ++++++++++++++++++
 src/backend/replication/logical/tablesync.c   |  13 +-
 src/backend/replication/repl_gram.y           |  32 +-
 src/backend/replication/repl_scanner.l        |   1 +
 src/backend/replication/slotfuncs.c           |   2 +-
 src/backend/replication/walsender.c           | 196 ++++++++-
 src/backend/utils/activity/wait_event.c       |   3 +
 src/backend/utils/misc/guc.c                  |  23 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/commands/subscriptioncmds.h       |   3 +
 src/include/nodes/nodes.h                     |   1 +
 src/include/nodes/replnodes.h                 |   9 +
 src/include/replication/logicalworker.h       |   9 +
 src/include/replication/slot.h                |   4 +-
 src/include/replication/walreceiver.h         |  16 +
 src/include/replication/worker_internal.h     |   8 +-
 src/include/utils/wait_event.h                |   1 +
 src/test/recovery/t/030_slot_sync.pl          |  58 +++
 25 files changed, 1148 insertions(+), 70 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c
 create mode 100644 src/test/recovery/t/030_slot_sync.pl

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index afbb6c35e3..2b2a21a251 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4406,6 +4406,23 @@ <title>Primary Server</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical replication slots that logical replication waits for.
+        If a logical replication connection is meant to switch to a physical
+        standby after the standby is promoted, the physical replication slot
+        for the standby should be listed here.  This ensures that logical
+        replication is not ahead of the physical standby.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
@@ -4794,6 +4811,23 @@ <title>Standby Servers</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-synchronize_slot_names" xreflabel="synchronize_slot_names">
+      <term><varname>synchronize_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>synchronize_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies a list of logical replication slots that a physical standby
+        should synchronize from the primary server.  This is necessary to be
+        able to retarget those logical replication connections to this standby
+        if it gets promoted.  Specify <literal>*</literal> to synchronize all
+        logical replication slots.  The default is empty.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 2b658080fe..7cdea20207 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -737,7 +737,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
 
 				RemoveSubscriptionRel(sub->oid, relid);
 
-				logicalrep_worker_stop(sub->oid, relid);
+				logicalrep_worker_stop(MyDatabaseId, sub->oid, relid);
 
 				/*
 				 * For READY state, we would have already dropped the
@@ -1239,7 +1239,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	{
 		LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc);
 
-		logicalrep_worker_stop(w->subid, w->relid);
+		logicalrep_worker_stop(w->dbid, w->subid, w->relid);
 	}
 	list_free(subworkers);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index c05f500639..818b8a35e9 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -128,6 +128,9 @@ static const struct
 	},
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
+	},
+	{
+		"ReplSlotSyncMain", ReplSlotSyncMain
 	}
 };
 
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index c08e599eef..57489a0d11 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static List *libpqrcv_list_slots(WalReceiverConn *conn, const char *slot_names);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -89,6 +91,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	libpqrcv_get_conninfo,
 	libpqrcv_get_senderinfo,
 	libpqrcv_identify_system,
+	libpqrcv_list_slots,
 	libpqrcv_server_version,
 	libpqrcv_readtimelinehistoryfile,
 	libpqrcv_startstreaming,
@@ -397,6 +400,99 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get list of slots from primary.
+ */
+static List *
+libpqrcv_list_slots(WalReceiverConn *conn, const char *slot_names)
+{
+	PGresult   *res;
+	List	   *slotlist = NIL;
+	int			ntuples;
+	StringInfoData s;
+	WalRecvReplicationSlotData *slot_data;
+
+	initStringInfo(&s);
+	appendStringInfoString(&s, "LIST_SLOTS");
+
+	if (strcmp(slot_names, "") != 0 && strcmp(slot_names, "*") != 0)
+	{
+		char	   *rawname;
+		List	   *namelist;
+		ListCell   *lc;
+
+		appendStringInfoChar(&s, ' ');
+		rawname = pstrdup(slot_names);
+		SplitIdentifierString(rawname, ',', &namelist);
+		foreach (lc, namelist)
+		{
+			if (lc != list_head(namelist))
+				appendStringInfoChar(&s, ',');
+			appendStringInfo(&s, "%s",
+							 quote_identifier(lfirst(lc)));
+		}
+	}
+
+	res = libpqrcv_PQexec(conn->streamConn, s.data);
+	pfree(s.data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("could not receive list of slots the primary server: %s",
+						pchomp(PQerrorMessage(conn->streamConn)))));
+	}
+	if (PQnfields(res) < 10)
+	{
+		int			nfields = PQnfields(res);
+
+		PQclear(res);
+		ereport(ERROR,
+				(errmsg("invalid response from primary server"),
+				 errdetail("Could not get list of slots: got %d fields, expected %d or more fields.",
+						   nfields, 10)));
+	}
+
+	ntuples = PQntuples(res);
+	for (int i = 0; i < ntuples; i++)
+	{
+		char	   *slot_type;
+
+		slot_data = palloc0(sizeof(WalRecvReplicationSlotData));
+		namestrcpy(&slot_data->name, PQgetvalue(res, i, 0));
+		if (!PQgetisnull(res, i, 1))
+			namestrcpy(&slot_data->plugin, PQgetvalue(res, i, 1));
+		slot_type = PQgetvalue(res, i, 2);
+		if (!PQgetisnull(res, i, 3))
+			slot_data->database = atooid(PQgetvalue(res, i, 3));
+		if (strcmp(slot_type, "physical") == 0)
+		{
+			if (OidIsValid(slot_data->database))
+				elog(ERROR, "unexpected physical replication slot with database set");
+		}
+		if (pg_strtoint32(PQgetvalue(res, i, 5)) == 1)
+			slot_data->persistency = RS_TEMPORARY;
+		else
+			slot_data->persistency = RS_PERSISTENT;
+		if (!PQgetisnull(res, i, 6))
+			slot_data->xmin = atooid(PQgetvalue(res, i, 6));
+		if (!PQgetisnull(res, i, 7))
+			slot_data->catalog_xmin = atooid(PQgetvalue(res, i, 7));
+		if (!PQgetisnull(res, i, 8))
+			slot_data->restart_lsn = pg_strtouint64(PQgetvalue(res, i, 8),
+													NULL, 10);
+		if (!PQgetisnull(res, i, 9))
+			slot_data->confirmed_flush = pg_strtouint64(PQgetvalue(res, i, 9),
+														NULL, 10);
+
+		slotlist = lappend(slotlist, slot_data);
+	}
+
+	PQclear(res);
+
+	return slotlist;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index c4e2fdeb71..bc3f23b5a2 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -24,6 +24,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 3fb4caa803..207ef9bc8b 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -22,6 +22,7 @@
 #include "access/htup_details.h"
 #include "access/tableam.h"
 #include "access/xact.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 #include "funcapi.h"
@@ -212,7 +213,7 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker,
  * subscription id and relid.
  */
 LogicalRepWorker *
-logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
+logicalrep_worker_find(Oid dbid, Oid subid, Oid relid, bool only_running)
 {
 	int			i;
 	LogicalRepWorker *res = NULL;
@@ -224,8 +225,8 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
-		if (w->in_use && w->subid == subid && w->relid == relid &&
-			(!only_running || w->proc))
+		if (w->in_use && w->dbid == dbid && w->subid == subid &&
+			w->relid == relid && (!only_running || w->proc))
 		{
 			res = w;
 			break;
@@ -275,9 +276,13 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	int			nsyncworkers;
 	TimestampTz now;
 
-	ereport(DEBUG1,
-			(errmsg_internal("starting logical replication worker for subscription \"%s\"",
-							 subname)));
+	if (OidIsValid(subid))
+		ereport(DEBUG1,
+				(errmsg_internal("starting logical replication worker for subscription \"%s\"",
+								 subname)));
+	else
+		ereport(DEBUG1,
+				(errmsg_internal("starting replication slot synchronization worker")));
 
 	/* Report this after the initial starting message for consistency. */
 	if (max_replication_slots == 0)
@@ -314,7 +319,9 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * reason we do this is because if some worker failed to start up and its
 	 * parent has crashed while waiting, the in_use state was never cleared.
 	 */
-	if (worker == NULL || nsyncworkers >= max_sync_workers_per_subscription)
+	if (worker == NULL ||
+		(OidIsValid(relid) &&
+		 nsyncworkers >= max_sync_workers_per_subscription))
 	{
 		bool		did_cleanup = false;
 
@@ -348,7 +355,7 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	 * silently as we might get here because of an otherwise harmless race
 	 * condition.
 	 */
-	if (nsyncworkers >= max_sync_workers_per_subscription)
+	if (OidIsValid(relid) && nsyncworkers >= max_sync_workers_per_subscription)
 	{
 		LWLockRelease(LogicalRepWorkerLock);
 		return;
@@ -395,15 +402,22 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	if (OidIsValid(subid))
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyWorkerMain");
+	else
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncMain");
 	if (OidIsValid(relid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u sync %u", subid, relid);
-	else
+	else if (OidIsValid(subid))
 		snprintf(bgw.bgw_name, BGW_MAXLEN,
 				 "logical replication worker for subscription %u", subid);
+	else
+		snprintf(bgw.bgw_name, BGW_MAXLEN,
+				 "replication slot synchronization worker");
+
 	snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication worker");
 
 	bgw.bgw_restart_time = BGW_NEVER_RESTART;
@@ -434,14 +448,14 @@ logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid,
  * it detaches from the slot.
  */
 void
-logicalrep_worker_stop(Oid subid, Oid relid)
+logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 	uint16		generation;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, false);
+	worker = logicalrep_worker_find(dbid, subid, relid, false);
 
 	/* No worker, nothing to do. */
 	if (!worker)
@@ -531,13 +545,13 @@ logicalrep_worker_stop(Oid subid, Oid relid)
  * Wake up (using latch) any logical replication worker for specified sub/rel.
  */
 void
-logicalrep_worker_wakeup(Oid subid, Oid relid)
+logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid)
 {
 	LogicalRepWorker *worker;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, true);
+	worker = logicalrep_worker_find(dbid, subid, relid, true);
 
 	if (worker)
 		logicalrep_worker_wakeup_ptr(worker);
@@ -714,7 +728,7 @@ ApplyLauncherRegister(void)
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState;
 	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
 	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ApplyLauncherMain");
 	snprintf(bgw.bgw_name, BGW_MAXLEN,
@@ -795,6 +809,116 @@ ApplyLauncherWakeup(void)
 		kill(LogicalRepCtx->launcher_pid, SIGUSR1);
 }
 
+static void
+ApplyLauncherStartSlotSync(TimestampTz *last_start_time, long *wait_time)
+{
+	WalReceiverConn *wrconn;
+	TimestampTz now;
+	char	   *err;
+	List	   *slots;
+	ListCell   *lc;
+	MemoryContext tmpctx;
+	MemoryContext oldctx;
+
+	if (strcmp(synchronize_slot_names, "") == 0)
+		return;
+
+	wrconn = walrcv_connect(PrimaryConnInfo, false,
+							"Logical Replication Launcher", &err);
+	if (!wrconn)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	/* Use temporary context for the slot list and worker info. */
+	tmpctx = AllocSetContextCreate(TopMemoryContext,
+								   "Logical Replication Launcher slot sync ctx",
+								   ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(tmpctx);
+
+	slots = walrcv_list_slots(wrconn, synchronize_slot_names);
+
+	now = GetCurrentTimestamp();
+
+	foreach(lc, slots)
+	{
+		WalRecvReplicationSlotData *slot_data = lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!OidIsValid(slot_data->database))
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(slot_data->database, InvalidOid,
+								   InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(slot_data->database, InvalidOid, NULL,
+									 BOOTSTRAP_SUPERUSERID, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(tmpctx);
+
+	walrcv_disconnect(wrconn);
+}
+
+static void
+ApplyLauncherStartSubs(TimestampTz *last_start_time, long *wait_time)
+{
+	TimestampTz now;
+	List	   *sublist;
+	ListCell   *lc;
+	MemoryContext subctx;
+	MemoryContext oldctx;
+
+	now = GetCurrentTimestamp();
+
+	/* Use temporary context for the database list and worker info. */
+	subctx = AllocSetContextCreate(TopMemoryContext,
+								   "Logical Replication Launcher sublist",
+								   ALLOCSET_DEFAULT_SIZES);
+	oldctx = MemoryContextSwitchTo(subctx);
+
+	/* search for subscriptions to start or stop. */
+	sublist = get_subscription_list();
+
+	/* Start the missing workers for enabled subscriptions. */
+	foreach(lc, sublist)
+	{
+		Subscription *sub = (Subscription *) lfirst(lc);
+		LogicalRepWorker *w;
+
+		if (!sub->enabled)
+			continue;
+
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		w = logicalrep_worker_find(sub->dbid, sub->oid, InvalidOid, false);
+		LWLockRelease(LogicalRepWorkerLock);
+
+		if (w == NULL)
+		{
+			*last_start_time = now;
+			*wait_time = wal_retrieve_retry_interval;
+
+			logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
+									 sub->owner, InvalidOid);
+		}
+	}
+
+	/* Switch back to original memory context. */
+	MemoryContextSwitchTo(oldctx);
+	/* Clean the temporary memory. */
+	MemoryContextDelete(subctx);
+}
+
 /*
  * Main loop for the apply launcher process.
  */
@@ -822,14 +946,12 @@ ApplyLauncherMain(Datum main_arg)
 	 */
 	BackgroundWorkerInitializeConnection(NULL, NULL, 0);
 
+	load_file("libpqwalreceiver", false);
+
 	/* Enter main loop */
 	for (;;)
 	{
 		int			rc;
-		List	   *sublist;
-		ListCell   *lc;
-		MemoryContext subctx;
-		MemoryContext oldctx;
 		TimestampTz now;
 		long		wait_time = DEFAULT_NAPTIME_PER_CYCLE;
 
@@ -841,42 +963,10 @@ ApplyLauncherMain(Datum main_arg)
 		if (TimestampDifferenceExceeds(last_start_time, now,
 									   wal_retrieve_retry_interval))
 		{
-			/* Use temporary context for the database list and worker info. */
-			subctx = AllocSetContextCreate(TopMemoryContext,
-										   "Logical Replication Launcher sublist",
-										   ALLOCSET_DEFAULT_SIZES);
-			oldctx = MemoryContextSwitchTo(subctx);
-
-			/* search for subscriptions to start or stop. */
-			sublist = get_subscription_list();
-
-			/* Start the missing workers for enabled subscriptions. */
-			foreach(lc, sublist)
-			{
-				Subscription *sub = (Subscription *) lfirst(lc);
-				LogicalRepWorker *w;
-
-				if (!sub->enabled)
-					continue;
-
-				LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-				w = logicalrep_worker_find(sub->oid, InvalidOid, false);
-				LWLockRelease(LogicalRepWorkerLock);
-
-				if (w == NULL)
-				{
-					last_start_time = now;
-					wait_time = wal_retrieve_retry_interval;
-
-					logicalrep_worker_launch(sub->dbid, sub->oid, sub->name,
-											 sub->owner, InvalidOid);
-				}
-			}
-
-			/* Switch back to original memory context. */
-			MemoryContextSwitchTo(oldctx);
-			/* Clean the temporary memory. */
-			MemoryContextDelete(subctx);
+			if (!RecoveryInProgress())
+				ApplyLauncherStartSubs(&last_start_time, &wait_time);
+			else
+				ApplyLauncherStartSlotSync(&last_start_time, &wait_time);
 		}
 		else
 		{
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 7aa5647a2c..f0b3b9ad87 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -95,11 +95,13 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/reorderbuffer.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"	/* just for SnapBuildSnapDecRefcount */
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
+#include "storage/ipc.h"
 #include "storage/sinval.h"
 #include "utils/builtins.h"
 #include "utils/combocid.h"
@@ -107,6 +109,7 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/relfilenodemap.h"
+#include "utils/varlena.h"
 
 
 /* entry for a hash table we use to map from xid to our transaction state */
@@ -2006,6 +2009,85 @@ ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	}
 }
 
+static void
+wait_for_standby_confirmation(XLogRecPtr commit_lsn)
+{
+	char	   *rawname;
+	List	   *namelist;
+	ListCell   *lc;
+	XLogRecPtr	flush_pos = InvalidXLogRecPtr;
+
+	if (strcmp(standby_slot_names, "") == 0)
+		return;
+
+	rawname = pstrdup(standby_slot_names);
+	SplitIdentifierString(rawname, ',', &namelist);
+
+	while (true)
+	{
+		int			wait_slots_remaining;
+		XLogRecPtr	oldest_flush_pos = InvalidXLogRecPtr;
+		int			rc;
+
+		wait_slots_remaining = list_length(namelist);
+
+		LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+		for (int i = 0; i < max_replication_slots; i++)
+		{
+			ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+			bool		inlist;
+
+			if (!s->in_use)
+				continue;
+
+			inlist = false;
+			foreach (lc, namelist)
+			{
+				char *name = lfirst(lc);
+				if (strcmp(name, NameStr(s->data.name)) == 0)
+				{
+					inlist = true;
+					break;
+				}
+			}
+			if (!inlist)
+				continue;
+
+			SpinLockAcquire(&s->mutex);
+
+			if (s->data.database == InvalidOid)
+				/* Physical slots advance restart_lsn on flush and ignore confirmed_flush_lsn */
+				flush_pos = s->data.restart_lsn;
+			else
+				/* For logical slots we must wait for commit and flush */
+				flush_pos = s->data.confirmed_flush;
+
+			SpinLockRelease(&s->mutex);
+
+			/* We want to find out the min(flush pos) over all named slots */
+			if (oldest_flush_pos == InvalidXLogRecPtr
+				|| oldest_flush_pos > flush_pos)
+				oldest_flush_pos = flush_pos;
+
+			if (flush_pos >= commit_lsn && wait_slots_remaining > 0)
+				wait_slots_remaining --;
+		}
+		LWLockRelease(ReplicationSlotControlLock);
+
+		if (wait_slots_remaining == 0)
+			return;
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   1000L, PG_WAIT_EXTENSION);
+
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+
+		CHECK_FOR_INTERRUPTS();
+	}
+}
+
 /*
  * Helper function for ReorderBufferReplay and ReorderBufferStreamTXN.
  *
@@ -2434,6 +2516,9 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			 * Call either PREPARE (for two-phase transactions) or COMMIT (for
 			 * regular ones).
 			 */
+
+			wait_for_standby_confirmation(commit_lsn);
+
 			if (rbtxn_prepared(txn))
 				rb->prepare(rb, txn, commit_lsn);
 			else
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..654ac154ea
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,412 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby from primary
+ *
+ * Copyright (c) 2016-2018, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "utils/builtins.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+char	   *synchronize_slot_names;
+char	   *standby_slot_names;
+
+/*
+ * Wait for remote slot to pass localy reserved position.
+ */
+static void
+wait_for_primary_slot_catchup(WalReceiverConn *wrconn, char *slot_name,
+							  XLogRecPtr min_lsn)
+{
+	WalRcvExecResult *res;
+	TupleTableSlot *slot;
+	Oid			slotRow[1] = {LSNOID};
+	StringInfoData cmd;
+	bool		isnull;
+	XLogRecPtr	restart_lsn;
+
+	for (;;)
+	{
+		int			rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		initStringInfo(&cmd);
+		appendStringInfo(&cmd,
+						 "SELECT restart_lsn"
+						 "  FROM pg_catalog.pg_replication_slots"
+						 " WHERE slot_name = %s",
+						 quote_literal_cstr(slot_name));
+		res = walrcv_exec(wrconn, cmd.data, 1, slotRow);
+
+		if (res->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					(errmsg("could not fetch slot info for slot \"%s\" from primary: %s",
+							slot_name, res->err)));
+
+		slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+		if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+			ereport(ERROR,
+					(errmsg("slot \"%s\" disapeared from provider",
+							slot_name)));
+
+		restart_lsn = DatumGetLSN(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		ExecClearTuple(slot);
+		walrcv_clear_result(res);
+
+		if (restart_lsn >= min_lsn)
+			break;
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This optionally creates new slot if there is no existing one.
+ */
+static void
+synchronize_one_slot(WalReceiverConn *wrconn, char *slot_name, char *database,
+					 char *plugin_name, XLogRecPtr target_lsn)
+{
+	bool		found = false;
+	XLogRecPtr	endlsn;
+
+	/* Search for the named slot and mark it active if we find it. */
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (!s->in_use)
+			continue;
+
+		if (strcmp(NameStr(s->data.name), slot_name) == 0)
+		{
+			found = true;
+			break;
+		}
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	StartTransactionCommand();
+
+	/* Already existing slot, acquire */
+	if (found)
+	{
+		ReplicationSlotAcquire(slot_name, true);
+
+		if (target_lsn < MyReplicationSlot->data.confirmed_flush)
+		{
+			elog(DEBUG1,
+				 "not synchronizing slot %s; synchronization would move it backward",
+				 slot_name);
+
+			ReplicationSlotRelease();
+			CommitTransactionCommand();
+			return;
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+		ReplicationSlot *slot;
+
+		ReplicationSlotCreate(slot_name, true, RS_EPHEMERAL, false);
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(database, false);
+		namestrcpy(&slot->data.plugin, plugin_name);
+		SpinLockRelease(&slot->mutex);
+
+		ReplicationSlotReserveWal();
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		if (target_lsn < MyReplicationSlot->data.restart_lsn)
+		{
+			ereport(LOG,
+					errmsg("waiting for remote slot \"%s\" LSN (%X/%X) to pass local slot LSN (%X/%X)",
+						   slot_name,
+						   LSN_FORMAT_ARGS(target_lsn), LSN_FORMAT_ARGS(MyReplicationSlot->data.restart_lsn)));
+
+			wait_for_primary_slot_catchup(wrconn, slot_name,
+										  MyReplicationSlot->data.restart_lsn);
+		}
+
+		ReplicationSlotPersist();
+	}
+
+	endlsn = pg_logical_replication_slot_advance(target_lsn);
+
+	elog(DEBUG3, "synchronized slot %s to lsn (%X/%X)",
+		 slot_name, LSN_FORMAT_ARGS(endlsn));
+
+	ReplicationSlotRelease();
+	CommitTransactionCommand();
+}
+
+static void
+synchronize_slots(void)
+{
+	WalRcvExecResult *res;
+	WalReceiverConn *wrconn = NULL;
+	TupleTableSlot *slot;
+	Oid			slotRow[3] = {TEXTOID, TEXTOID, LSNOID};
+	StringInfoData s;
+	char	   *database;
+	char	   *err;
+	MemoryContext oldctx = CurrentMemoryContext;
+
+	if (!WalRcv)
+		return;
+
+	/* syscache access needs a transaction env. */
+	StartTransactionCommand();
+	/* make dbname live outside TX context */
+	MemoryContextSwitchTo(oldctx);
+
+	database = get_database_name(MyDatabaseId);
+	initStringInfo(&s);
+	appendStringInfo(&s, "%s dbname=%s", PrimaryConnInfo, database);
+	wrconn = walrcv_connect(s.data, true, "slot_sync", &err);
+
+	if (wrconn == NULL)
+		ereport(ERROR,
+				(errmsg("could not connect to the primary server: %s", err)));
+
+	resetStringInfo(&s);
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn"
+					 "  FROM pg_catalog.pg_replication_slots"
+					 " WHERE database = %s",
+					 quote_literal_cstr(database));
+	if (strcmp(synchronize_slot_names, "") != 0 && strcmp(synchronize_slot_names, "*") != 0)
+	{
+		char	   *rawname;
+		List	   *namelist;
+		ListCell   *lc;
+
+		rawname = pstrdup(synchronize_slot_names);
+		SplitIdentifierString(rawname, ',', &namelist);
+
+		appendStringInfoString(&s, " AND slot_name IN (");
+		foreach (lc, namelist)
+		{
+			if (lc != list_head(namelist))
+				appendStringInfoChar(&s, ',');
+			appendStringInfo(&s, "%s",
+							 quote_literal_cstr(lfirst(lc)));
+		}
+		appendStringInfoChar(&s, ')');
+	}
+
+	res = walrcv_exec(wrconn, s.data, 3, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				(errmsg("could not fetch slot info from primary: %s",
+						res->err)));
+
+	CommitTransactionCommand();
+	/* CommitTransactionCommand switches to TopMemoryContext */
+	MemoryContextSwitchTo(oldctx);
+
+	slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+	{
+		char	   *slot_name;
+		char	   *plugin_name;
+		XLogRecPtr	confirmed_flush_lsn;
+		bool		isnull;
+
+		slot_name = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+		Assert(!isnull);
+
+		plugin_name = TextDatumGetCString(slot_getattr(slot, 2, &isnull));
+		Assert(!isnull);
+
+		confirmed_flush_lsn = DatumGetLSN(slot_getattr(slot, 3, &isnull));
+		Assert(!isnull);
+
+		synchronize_one_slot(wrconn, slot_name, database, plugin_name,
+							 confirmed_flush_lsn);
+
+		ExecClearTuple(slot);
+	}
+
+	walrcv_clear_result(res);
+	pfree(database);
+
+	walrcv_disconnect(wrconn);
+}
+
+/*
+ * The main loop of our worker process.
+ */
+void
+ReplSlotSyncMain(Datum main_arg)
+{
+	int			worker_slot = DatumGetInt32(main_arg);
+
+	/* Attach to slot */
+	logicalrep_worker_attach(worker_slot);
+
+	/* Establish signal handlers. */
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	/* Connect to our database. */
+	BackgroundWorkerInitializeConnectionByOid(MyLogicalRepWorker->dbid,
+											  MyLogicalRepWorker->userid,
+											  0);
+
+	StartTransactionCommand();
+	ereport(LOG,
+			(errmsg("replication slot synchronization worker for database \"%s\" has started",
+					get_database_name(MyLogicalRepWorker->dbid))));
+	CommitTransactionCommand();
+
+	/* Main wait loop. */
+	for (;;)
+	{
+		int			rc;
+
+		CHECK_FOR_INTERRUPTS();
+
+		if (!RecoveryInProgress())
+			return;
+
+		if (strcmp(synchronize_slot_names, "") == 0)
+			return;
+
+		synchronize_slots();
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   wal_retrieve_retry_interval,
+					   WAIT_EVENT_REPL_SLOT_SYNC_MAIN);
+
+		ResetLatch(MyLatch);
+
+		/* emergency bailout if postmaster has died */
+		if (rc & WL_POSTMASTER_DEATH)
+			proc_exit(1);
+	}
+}
+
+/*
+ * Routines for handling the GUC variable(s)
+ */
+
+bool
+check_synchronize_slot_names(char **newval, void **extra, GucSource source)
+{
+	/* Special handling for "*" which means all. */
+	if (strcmp(*newval, "*") == 0)
+	{
+		return true;
+	}
+	else
+	{
+		char	   *rawname;
+		List	   *namelist;
+		ListCell   *lc;
+
+		/* Need a modifiable copy of string */
+		rawname = pstrdup(*newval);
+
+		/* Parse string into list of identifiers */
+		if (!SplitIdentifierString(rawname, ',', &namelist))
+		{
+			/* syntax error in name list */
+			GUC_check_errdetail("List syntax is invalid.");
+			pfree(rawname);
+			list_free(namelist);
+			return false;
+		}
+
+		foreach(lc, namelist)
+		{
+			char	   *curname = (char *) lfirst(lc);
+
+			ReplicationSlotValidateName(curname, ERROR);
+		}
+
+		pfree(rawname);
+		list_free(namelist);
+	}
+
+	return true;
+}
+
+
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	char	   *rawname;
+	List	   *namelist;
+	ListCell   *lc;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Parse string into list of identifiers */
+	if (!SplitIdentifierString(rawname, ',', &namelist))
+	{
+		/* syntax error in name list */
+		GUC_check_errdetail("List syntax is invalid.");
+		pfree(rawname);
+		list_free(namelist);
+		return false;
+	}
+
+	foreach(lc, namelist)
+	{
+		char	   *curname = (char *) lfirst(lc);
+
+		ReplicationSlotValidateName(curname, ERROR);
+	}
+
+	pfree(rawname);
+	list_free(namelist);
+
+	return true;
+}
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f07983a43c..0e0593f716 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -100,6 +100,7 @@
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_type.h"
 #include "commands/copy.h"
+#include "commands/subscriptioncmds.h"
 #include "miscadmin.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -151,7 +152,8 @@ finish_sync_worker(void)
 	CommitTransactionCommand();
 
 	/* Find the main apply worker and signal it. */
-	logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid);
+	logicalrep_worker_wakeup(MyLogicalRepWorker->dbid,
+							 MyLogicalRepWorker->subid, InvalidOid);
 
 	/* Stop gracefully */
 	proc_exit(0);
@@ -191,7 +193,8 @@ wait_for_relation_state_change(Oid relid, char expected_state)
 
 		/* Check if the sync worker is still running and bail if not. */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid, relid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid, relid,
 										false);
 		LWLockRelease(LogicalRepWorkerLock);
 		if (!worker)
@@ -238,7 +241,8 @@ wait_for_worker_state_change(char expected_state)
 		 * waiting.
 		 */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-		worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+		worker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+										MyLogicalRepWorker->subid,
 										InvalidOid, false);
 		if (worker && worker->proc)
 			logicalrep_worker_wakeup_ptr(worker);
@@ -484,7 +488,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn)
 			 */
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-			syncworker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+			syncworker = logicalrep_worker_find(MyLogicalRepWorker->dbid,
+												MyLogicalRepWorker->subid,
 												rstate->relid, false);
 
 			if (syncworker)
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index dcb1108579..902841efe6 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -91,11 +91,12 @@ static SQLCmd *make_sqlcmd(void);
 %token K_USE_SNAPSHOT
 %token K_MANIFEST
 %token K_MANIFEST_CHECKSUMS
+%token K_LIST_SLOTS
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
 				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show sql_cmd
+				read_replication_slot timeline_history show sql_cmd list_slots
 %type <list>	base_backup_legacy_opt_list generic_option_list
 %type <defelt>	base_backup_legacy_opt generic_option
 %type <uintval>	opt_timeline
@@ -106,6 +107,7 @@ static SQLCmd *make_sqlcmd(void);
 %type <boolval>	opt_temporary
 %type <list>	create_slot_options create_slot_legacy_opt_list
 %type <defelt>	create_slot_legacy_opt
+%type <list>	slot_name_list slot_name_list_opt
 
 %%
 
@@ -129,6 +131,7 @@ command:
 			| read_replication_slot
 			| timeline_history
 			| show
+			| list_slots
 			| sql_cmd
 			;
 
@@ -142,6 +145,33 @@ identify_system:
 				}
 			;
 
+slot_name_list:
+			IDENT
+				{
+					$$ = list_make1($1);
+				}
+			| slot_name_list ',' IDENT
+				{
+					$$ = lappend($1, $3);
+				}
+
+slot_name_list_opt:
+			slot_name_list			{ $$ = $1; }
+			| /* EMPTY */			{ $$ = NIL; }
+		;
+
+/*
+ * LIST_SLOTS
+ */
+list_slots:
+			K_LIST_SLOTS slot_name_list_opt
+				{
+					ListSlotsCmd *cmd = makeNode(ListSlotsCmd);
+					cmd->slot_names = $2;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * READ_REPLICATION_SLOT %s
  */
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 1b599c255e..9ee638355d 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -85,6 +85,7 @@ identifier		{ident_start}{ident_cont}*
 BASE_BACKUP			{ return K_BASE_BACKUP; }
 FAST			{ return K_FAST; }
 IDENTIFY_SYSTEM		{ return K_IDENTIFY_SYSTEM; }
+LIST_SLOTS		{ return K_LIST_SLOTS; }
 READ_REPLICATION_SLOT	{ return K_READ_REPLICATION_SLOT; }
 SHOW		{ return K_SHOW; }
 LABEL			{ return K_LABEL; }
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index d11daeb1fc..e93fea55ad 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -484,7 +484,7 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
  * WAL and removal of old catalog tuples.  As decoding is done in fast_forward
  * mode, no changes are generated anyway.
  */
-static XLogRecPtr
+XLogRecPtr
 pg_logical_replication_slot_advance(XLogRecPtr moveto)
 {
 	LogicalDecodingContext *ctx;
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 84915ed95b..2fce290ed6 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -456,6 +456,194 @@ IdentifySystem(void)
 	end_tup_output(tstate);
 }
 
+static int
+pg_qsort_namecmp(const void *a, const void *b)
+{
+	return strncmp(NameStr(*(Name) a), NameStr(*(Name) b), NAMEDATALEN);
+}
+
+/*
+ * Handle the LIST_SLOTS command.
+ */
+static void
+ListSlots(ListSlotsCmd *cmd)
+{
+	DestReceiver *dest;
+	TupOutputState *tstate;
+	TupleDesc	tupdesc;
+	NameData   *slot_names;
+	int			numslot_names;
+
+	numslot_names = list_length(cmd->slot_names);
+	if (numslot_names)
+	{
+		ListCell   *lc;
+		int			i = 0;
+
+		slot_names = palloc(numslot_names * sizeof(NameData));
+		foreach(lc, cmd->slot_names)
+		{
+			char	   *slot_name = lfirst(lc);
+
+			ReplicationSlotValidateName(slot_name, ERROR);
+			namestrcpy(&slot_names[i++], slot_name);
+		}
+
+		qsort(slot_names, numslot_names, sizeof(NameData), pg_qsort_namecmp);
+	}
+
+	dest = CreateDestReceiver(DestRemoteSimple);
+
+	/* need a tuple descriptor representing four columns */
+	tupdesc = CreateTemplateTupleDesc(10);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "slot_name",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "plugin",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "slot_type",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 4, "datoid",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 5, "database",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 6, "temporary",
+							  INT4OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 7, "xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 8, "catalog_xmin",
+							  INT8OID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 9, "restart_lsn",
+							  TEXTOID, -1, 0);
+	TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 10, "confirmed_flush",
+							  TEXTOID, -1, 0);
+
+	/* prepare for projection of tuples */
+	tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual);
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (int slotno = 0; slotno < max_replication_slots; slotno++)
+	{
+		ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno];
+		char		restart_lsn_str[MAXFNAMELEN];
+		char		confirmed_flush_lsn_str[MAXFNAMELEN];
+		Datum		values[10];
+		bool		nulls[10];
+
+		ReplicationSlotPersistency persistency;
+		TransactionId xmin;
+		TransactionId catalog_xmin;
+		XLogRecPtr	restart_lsn;
+		XLogRecPtr	confirmed_flush_lsn;
+		Oid			datoid;
+		NameData	slot_name;
+		NameData	plugin;
+		int			i;
+		int64		tmpbigint;
+
+		if (!slot->in_use)
+			continue;
+
+		SpinLockAcquire(&slot->mutex);
+
+		xmin = slot->data.xmin;
+		catalog_xmin = slot->data.catalog_xmin;
+		datoid = slot->data.database;
+		restart_lsn = slot->data.restart_lsn;
+		confirmed_flush_lsn = slot->data.confirmed_flush;
+		namestrcpy(&slot_name, NameStr(slot->data.name));
+		namestrcpy(&plugin, NameStr(slot->data.plugin));
+		persistency = slot->data.persistency;
+
+		SpinLockRelease(&slot->mutex);
+
+		if (numslot_names &&
+			!bsearch((void *) &slot_name, (void *) slot_names,
+					 numslot_names, sizeof(NameData), pg_qsort_namecmp))
+			continue;
+
+		memset(nulls, 0, sizeof(nulls));
+
+		i = 0;
+		values[i++] = CStringGetTextDatum(NameStr(slot_name));
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+			values[i++] = CStringGetTextDatum(NameStr(plugin));
+
+		if (datoid == InvalidOid)
+			values[i++] = CStringGetTextDatum("physical");
+		else
+			values[i++] = CStringGetTextDatum("logical");
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			tmpbigint = datoid;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+
+		if (datoid == InvalidOid)
+			nulls[i++] = true;
+		else
+		{
+			MemoryContext cur = CurrentMemoryContext;
+
+			/* syscache access needs a transaction env. */
+			StartTransactionCommand();
+			/* make dbname live outside TX context */
+			MemoryContextSwitchTo(cur);
+			values[i++] = CStringGetTextDatum(get_database_name(datoid));
+			CommitTransactionCommand();
+			/* CommitTransactionCommand switches to TopMemoryContext */
+			MemoryContextSwitchTo(cur);
+		}
+
+		values[i++] = Int32GetDatum(persistency == RS_TEMPORARY ? 1 : 0);
+
+		if (xmin != InvalidTransactionId)
+		{
+			tmpbigint = xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (catalog_xmin != InvalidTransactionId)
+		{
+			tmpbigint = catalog_xmin;
+			values[i++] = Int64GetDatum(tmpbigint);
+		}
+		else
+			nulls[i++] = true;
+
+		if (restart_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(restart_lsn_str, sizeof(restart_lsn_str), "%X/%X",
+					 LSN_FORMAT_ARGS(restart_lsn));
+			values[i++] = CStringGetTextDatum(restart_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		if (confirmed_flush_lsn != InvalidXLogRecPtr)
+		{
+			snprintf(confirmed_flush_lsn_str, sizeof(confirmed_flush_lsn_str),
+					 "%X/%X", LSN_FORMAT_ARGS(confirmed_flush_lsn));
+			values[i++] = CStringGetTextDatum(confirmed_flush_lsn_str);
+		}
+		else
+			nulls[i++] = true;
+
+		/* send it to dest */
+		do_tup_output(tstate, values, nulls);
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	end_tup_output(tstate);
+}
+
 /* Handle READ_REPLICATION_SLOT command */
 static void
 ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
@@ -554,7 +742,6 @@ ReadReplicationSlot(ReadReplicationSlotCmd *cmd)
 	end_tup_output(tstate);
 }
 
-
 /*
  * Handle TIMELINE_HISTORY command.
  */
@@ -1749,6 +1936,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_ListSlotsCmd:
+			cmdtag = "LIST_SLOTS";
+			set_ps_display(cmdtag);
+			ListSlots((ListSlotsCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4d53f040e8..6922353c94 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -230,6 +230,9 @@ pgstat_get_wait_activity(WaitEventActivity w)
 		case WAIT_EVENT_LOGICAL_LAUNCHER_MAIN:
 			event_name = "LogicalLauncherMain";
 			break;
+		case WAIT_EVENT_REPL_SLOT_SYNC_MAIN:
+			event_name = "ReplSlotSyncMain";
+			break;
 		case WAIT_EVENT_PGSTAT_MAIN:
 			event_name = "PgStatMain";
 			break;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index f736e8d872..ff1eab0207 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -75,6 +75,7 @@
 #include "postmaster/syslogger.h"
 #include "postmaster/walwriter.h"
 #include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
 #include "replication/reorderbuffer.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
@@ -4638,6 +4639,28 @@ static struct config_string ConfigureNamesString[] =
 		check_backtrace_functions, assign_backtrace_functions, NULL
 	},
 
+	{
+		{"synchronize_slot_names", PGC_SIGHUP, REPLICATION_STANDBY,
+			gettext_noop("Sets the names of replication slots which to synchronize from primary to standby."),
+			gettext_noop("Value of \"*\" means all."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&synchronize_slot_names,
+		"",
+		check_synchronize_slot_names, NULL, NULL
+	},
+
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("List of physical slots that must confirm changes before changes are sent to logical replication consumers."),
+			NULL,
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a1acd46b61..e8b5f76125 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -315,6 +315,7 @@
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
 #vacuum_defer_cleanup_age = 0	# number of xacts by which cleanup is delayed
+#standby_slot_names = ''	# physical standby slot names that logical replication waits for
 
 # - Standby Servers -
 
@@ -343,6 +344,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#synchronize_slot_names = ''		# logical replication slots to sync to standby
 
 # - Subscribers -
 
diff --git a/src/include/commands/subscriptioncmds.h b/src/include/commands/subscriptioncmds.h
index aec7e478ab..1cc19e0c99 100644
--- a/src/include/commands/subscriptioncmds.h
+++ b/src/include/commands/subscriptioncmds.h
@@ -17,6 +17,7 @@
 
 #include "catalog/objectaddress.h"
 #include "parser/parse_node.h"
+#include "replication/walreceiver.h"
 
 extern ObjectAddress CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 										bool isTopLevel);
@@ -26,4 +27,6 @@ extern void DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel);
 extern ObjectAddress AlterSubscriptionOwner(const char *name, Oid newOwnerId);
 extern void AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId);
 
+extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
+
 #endif							/* SUBSCRIPTIONCMDS_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 7c657c1241..920a510c4c 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -501,6 +501,7 @@ typedef enum NodeTag
 	T_StartReplicationCmd,
 	T_TimeLineHistoryCmd,
 	T_SQLCmd,
+	T_ListSlotsCmd,
 
 	/*
 	 * TAGS FOR RANDOM OTHER STUFF
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index a746fafc12..3f81409e50 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -33,6 +33,15 @@ typedef struct IdentifySystemCmd
 	NodeTag		type;
 } IdentifySystemCmd;
 
+/* ----------------------
+ *		LIST_SLOTS command
+ * ----------------------
+ */
+typedef struct ListSlotsCmd
+{
+	NodeTag		type;
+	List	   *slot_names;
+} ListSlotsCmd;
 
 /* ----------------------
  *		BASE_BACKUP command
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index 2ad61a001a..f5b5ef07e8 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -12,8 +12,17 @@
 #ifndef LOGICALWORKER_H
 #define LOGICALWORKER_H
 
+#include "utils/guc.h"
+
+extern char *synchronize_slot_names;
+extern char *standby_slot_names;
+
 extern void ApplyWorkerMain(Datum main_arg);
+extern void ReplSlotSyncMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 
+extern bool check_synchronize_slot_names(char **newval, void **extra, GucSource source);
+extern bool check_standby_slot_names(char **newval, void **extra, GucSource source);
+
 #endif							/* LOGICALWORKER_H */
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 53d773ccff..a29e517707 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -216,7 +216,6 @@ extern void ReplicationSlotsDropDBSlots(Oid dboid);
 extern bool InvalidateObsoleteReplicationSlots(XLogSegNo oldestSegno);
 extern ReplicationSlot *SearchNamedReplicationSlot(const char *name, bool need_lock);
 extern void ReplicationSlotNameForTablesync(Oid suboid, Oid relid, char *syncslotname, int szslot);
-extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok);
 
 extern void StartupReplicationSlots(void);
 extern void CheckPointReplicationSlots(void);
@@ -224,4 +223,7 @@ extern void CheckPointReplicationSlots(void);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern XLogRecPtr pg_logical_replication_slot_advance(XLogRecPtr moveto);
+
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0b607ed777..dbeb447e7a 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -18,6 +18,7 @@
 #include "pgtime.h"
 #include "port/atomics.h"
 #include "replication/logicalproto.h"
+#include "replication/slot.h"
 #include "replication/walsender.h"
 #include "storage/condition_variable.h"
 #include "storage/latch.h"
@@ -187,6 +188,13 @@ typedef struct
 	}			proto;
 } WalRcvStreamOptions;
 
+/*
+ * Slot information receiver from remote.
+ *
+ * Currently this is same as ReplicationSlotPersistentData
+ */
+#define WalRecvReplicationSlotData ReplicationSlotPersistentData
+
 struct WalReceiverConn;
 typedef struct WalReceiverConn WalReceiverConn;
 
@@ -274,6 +282,11 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * TODO
+ */
+typedef List *(*walrcv_list_slots_fn) (WalReceiverConn *conn, const char *slots);
+
 /*
  * walrcv_server_version_fn
  *
@@ -387,6 +400,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_list_slots_fn walrcv_list_slots;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -411,6 +425,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_list_slots(conn, slots) \
+	WalReceiverFunctions->walrcv_list_slots(conn, slots)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 9d29849d80..5de7f80e79 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -58,7 +58,7 @@ typedef struct LogicalRepWorker
 	 * exits.  Under this, separate buffiles would be created for each
 	 * transaction which will be deleted after the transaction is finished.
 	 */
-	FileSet    *stream_fileset;
+	struct FileSet *stream_fileset;
 
 	/* Stats. */
 	XLogRecPtr	last_lsn;
@@ -81,13 +81,13 @@ extern LogicalRepWorker *MyLogicalRepWorker;
 extern bool in_remote_transaction;
 
 extern void logicalrep_worker_attach(int slot);
-extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
+extern LogicalRepWorker *logicalrep_worker_find(Oid dbid, Oid subid, Oid relid,
 												bool only_running);
 extern List *logicalrep_workers_find(Oid subid, bool only_running);
 extern void logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname,
 									 Oid userid, Oid relid);
-extern void logicalrep_worker_stop(Oid subid, Oid relid);
-extern void logicalrep_worker_wakeup(Oid subid, Oid relid);
+extern void logicalrep_worker_stop(Oid dbid, Oid subid, Oid relid);
+extern void logicalrep_worker_wakeup(Oid dbid, Oid subid, Oid relid);
 extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker);
 
 extern int	logicalrep_sync_worker_count(Oid subid);
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 8785a8e12c..3274eade53 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -42,6 +42,7 @@ typedef enum
 	WAIT_EVENT_CHECKPOINTER_MAIN,
 	WAIT_EVENT_LOGICAL_APPLY_MAIN,
 	WAIT_EVENT_LOGICAL_LAUNCHER_MAIN,
+	WAIT_EVENT_REPL_SLOT_SYNC_MAIN,
 	WAIT_EVENT_PGSTAT_MAIN,
 	WAIT_EVENT_RECOVERY_WAL_STREAM,
 	WAIT_EVENT_SYSLOGGER_MAIN,
diff --git a/src/test/recovery/t/030_slot_sync.pl b/src/test/recovery/t/030_slot_sync.pl
new file mode 100644
index 0000000000..c87e7dc016
--- /dev/null
+++ b/src/test/recovery/t/030_slot_sync.pl
@@ -0,0 +1,58 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 2;
+
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 'logical');
+$node_primary->append_conf('postgresql.conf', "standby_slot_names = 'pslot1'");
+$node_primary->start;
+$node_primary->psql('postgres', q{SELECT pg_create_physical_replication_slot('pslot1');});
+
+$node_primary->backup('backup');
+
+my $node_phys_standby = PostgreSQL::Test::Cluster->new('phys_standby');
+$node_phys_standby->init_from_backup($node_primary, 'backup', has_streaming => 1);
+$node_phys_standby->append_conf('postgresql.conf', "synchronize_slot_names = '*'");
+$node_phys_standby->append_conf('postgresql.conf', "primary_slot_name = 'pslot1'");
+$node_phys_standby->start;
+
+$node_primary->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+$node_primary->safe_psql('postgres', "INSERT INTO t1 VALUES (1), (2), (3)");
+
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$node_subscriber->safe_psql('postgres', "CREATE TABLE t1 (a int PRIMARY KEY)");
+
+$node_primary->safe_psql('postgres', "CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '" . ($node_primary->connstr . ' dbname=postgres') . "' PUBLICATION pub1");
+
+# Wait for initial sync of all subscriptions
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+my $result = $node_primary->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots WHERE slot_type = 'logical'");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on primary');
+
+# FIXME: standby needs restart to pick up new slots
+$node_phys_standby->restart;
+sleep 3;
+
+$result = $node_phys_standby->safe_psql('postgres',
+	"SELECT slot_name, plugin, database FROM pg_replication_slots");
+
+is($result, qq(sub1|pgoutput|postgres), 'logical slot on standby');
+
+$node_primary->safe_psql('postgres', "INSERT INTO t1 VALUES (4), (5), (6)");
+$node_primary->wait_for_catchup('sub1');

base-commit: ece8c76192fee0b78509688325631ceabca44ff5
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-12-14 22:13  Peter Eisentraut <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Peter Eisentraut @ 2021-12-14 22:13 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: pgsql-hackers

On 24.11.21 07:11, Masahiko Sawada wrote:
> I haven’t looked at the patch deeply but regarding 007_sync_rep.pl,
> the tests seem to fail since the tests rely on the order of the wal
> sender array on the shared memory. Since a background worker for
> synchronizing replication slots periodically connects to the walsender
> on the primary and disconnects, it breaks the assumption of the order.
> Regarding 010_logical_decoding_timelines.pl, I guess that the patch
> breaks the test because the background worker for synchronizing slots
> on the replica periodically advances the replica's slot. I think we
> need to have a way to disable the slot synchronization or to specify
> the slot name to sync with the primary. I'm not sure we already
> discussed this topic but I think we need it at least for testing
> purposes.

This has been addressed by patch v2 that adds such a setting.






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-12-14 22:19  Peter Eisentraut <[email protected]>
  parent: Dimitri Fontaine <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Peter Eisentraut @ 2021-12-14 22:19 UTC (permalink / raw)
  To: Dimitri Fontaine <[email protected]>; +Cc: pgsql-hackers

On 24.11.21 17:25, Dimitri Fontaine wrote:
> Is there a case to be made about doing the same thing for physical
> replication slots too?

It has been considered.  At the moment, I'm not doing it, because it 
would add more code and complexity and it's not that important.  But it 
could be added in the future.

> Given the admitted state of the patch, I didn't focus on tests. I could
> successfully apply the patch on-top of current master's branch, and
> cleanly compile and `make check`.
> 
> Then I also updated pg_auto_failover to support Postgres 15devel [2] so
> that I could then `make NODES=3 cluster` there and play with the new
> replication command:
> 
>    $ psql -d "port=5501 replication=1" -c "LIST_SLOTS;"
>    psql:/Users/dim/.psqlrc:24: ERROR:  XX000: cannot execute SQL commands in WAL sender for physical replication
>    LOCATION:  exec_replication_command, walsender.c:1830
>    ...
> 
> I'm not too sure about this idea of running SQL in a replication
> protocol connection that you're mentioning, but I suppose that's just me
> needing to brush up on the topic.

FWIW, the way the replication command parser works, if there is a parse 
error, it tries to interpret the command as a plain SQL command.  But 
that only works for logical replication connections.  So in physical 
replication, if you try to run anything that does not parse, you will 
get this error.  But that has nothing to do with this feature.  The 
above command works for me, so maybe something else went wrong in your 
situation.

> Maybe the first question about configuration would be about selecting
> which slots a standby should maintain from the primary. Is it all of the
> slots that exists on both the nodes, or a sublist of that?
> 
> Is it possible to have a slot with the same name on a primary and a
> standby node, in a way that the standby's slot would be a completely
> separate entity from the primary's slot? If yes (I just don't know at
> the moment), well then, should we continue to allow that?

This has been added in v2.

> Also, do we want to even consider having the slot management on a
> primary node depend on the ability to sync the advancing on one or more
> standby nodes? I'm not sure to see that one as a good idea, but maybe we
> want to kill it publically very early then ;-)

I don't know what you mean by this.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-12-14 22:24  Peter Eisentraut <[email protected]>
  parent: Bharath Rupireddy <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Peter Eisentraut @ 2021-12-14 22:24 UTC (permalink / raw)
  To: Bharath Rupireddy <[email protected]>; +Cc: pgsql-hackers

On 28.11.21 07:52, Bharath Rupireddy wrote:
> 1) Instead of a new LIST_SLOT command, can't we use
> READ_REPLICATION_SLOT (slight modifications needs to be done to make
> it support logical replication slots and to get more information from
> the subscriber).

I looked at that but didn't see an obvious way to consolidate them. 
This is something we could look at again later.

> 2) How frequently the new bg worker is going to sync the slot info?
> How can it ensure that the latest information exists say when the
> subscriber is down/crashed before it picks up the latest slot
> information?

The interval is currently hardcoded, but could be a configuration 
setting.  In the v2 patch, there is a new setting that orders physical 
replication before logical so that the logical subscribers cannot get 
ahead of the physical standby.

> 3) Instead of the subscriber pulling the slot info, why can't the
> publisher (via the walsender or a new bg worker maybe?) push the
> latest slot info? I'm not sure we want to add more functionality to
> the walsender, if yes, isn't it going to be much simpler?

This sounds like the failover slot feature, which was rejected.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2021-12-16 02:15  Hsu, John <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Hsu, John @ 2021-12-16 02:15 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; Bharath Rupireddy <[email protected]>; +Cc: pgsql-hackers

Hello,

I started taking a brief look at the v2 patch, and it does appear to work for the basic case. Logical slot is synchronized across and I can connect to the promoted standby and stream changes afterwards.

It's not clear to me what the correct behavior is when a logical slot that has been synced to the replica and then it gets deleted on the writer. Would we expect this to be propagated or leave it up to the end-user to manage?

> +       rawname = pstrdup(standby_slot_names);
> +       SplitIdentifierString(rawname, ',', &namelist);
> +
> +       while (true)
> +       {
> +               int                     wait_slots_remaining;
> +               XLogRecPtr      oldest_flush_pos = InvalidXLogRecPtr;
> +               int                     rc;
> +
> +               wait_slots_remaining = list_length(namelist);
> +
> +               LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
> +               for (int i = 0; i < max_replication_slots; i++)
> +               {

Even though standby_slot_names is PGC_SIGHUP, we never reload/re-process the value. If we have a wrong entry in there, the backend becomes stuck until we re-establish the logical connection. Adding "postmaster/interrupt.h" with ConfigReloadPending / ProcessConfigFile does seem to work.

Another thing I noticed is that once it starts waiting in this block, Ctrl+C doesn't seem to terminate the backend?

pg_recvlogical -d postgres -p 5432 --slot regression_slot --start -f -
..
^Cpg_recvlogical: error: unexpected termination of replication stream: 

The logical backend connection is still present:

ps aux | grep 51263
   hsuchen 51263 80.7  0.0 320180 14304 ?        Rs   01:11   3:04 postgres: walsender hsuchen [local] START_REPLICATION

pstack 51263
#0  0x00007ffee99e79a5 in clock_gettime ()
#1  0x00007f8705e88246 in clock_gettime () from /lib64/libc.so.6
#2  0x000000000075f141 in WaitEventSetWait ()
#3  0x000000000075f565 in WaitLatch ()
#4  0x0000000000720aea in ReorderBufferProcessTXN ()
#5  0x00000000007142a6 in DecodeXactOp ()
#6  0x000000000071460f in LogicalDecodingProcessRecord ()

It can be terminated with a pg_terminate_backend though.

If we have a physical slot with name foo on the standby, and then a logical slot is created on the writer with the same slot_name it does error out on the replica although it prevents other slots from being synchronized which is probably fine.

2021-12-16 02:10:29.709 UTC [73788] LOG:  replication slot synchronization worker for database "postgres" has started
2021-12-16 02:10:29.713 UTC [73788] ERROR:  cannot use physical replication slot for logical decoding
2021-12-16 02:10:29.714 UTC [73037] DEBUG:  unregistering background worker "replication slot synchronization worker"

On 12/14/21, 2:26 PM, "Peter Eisentraut" <[email protected]> wrote:

    CAUTION: This email originated from outside of the organization. Do not click links or open attachments unless you can confirm the sender and know the content is safe.



    On 28.11.21 07:52, Bharath Rupireddy wrote:
    > 1) Instead of a new LIST_SLOT command, can't we use
    > READ_REPLICATION_SLOT (slight modifications needs to be done to make
    > it support logical replication slots and to get more information from
    > the subscriber).

    I looked at that but didn't see an obvious way to consolidate them.
    This is something we could look at again later.

    > 2) How frequently the new bg worker is going to sync the slot info?
    > How can it ensure that the latest information exists say when the
    > subscriber is down/crashed before it picks up the latest slot
    > information?

    The interval is currently hardcoded, but could be a configuration
    setting.  In the v2 patch, there is a new setting that orders physical
    replication before logical so that the logical subscribers cannot get
    ahead of the physical standby.

    > 3) Instead of the subscriber pulling the slot info, why can't the
    > publisher (via the walsender or a new bg worker maybe?) push the
    > latest slot info? I'm not sure we want to add more functionality to
    > the walsender, if yes, isn't it going to be much simpler?

    This sounds like the failover slot feature, which was rejected.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2022-11-15 09:02  Drouvot, Bertrand <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Drouvot, Bertrand @ 2022-11-15 09:02 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; +Cc: Andres Freund <[email protected]>; pgsql-hackers

Hi,

On 2/11/22 3:26 PM, Peter Eisentraut wrote:
> On 10.02.22 22:47, Bruce Momjian wrote:
>> On Tue, Feb  8, 2022 at 08:27:32PM +0530, Ashutosh Sharma wrote:
>>>> Which means that if e.g. the standby_slot_names GUC differs from
>>>> synchronize_slot_names on the physical replica, the slots 
>>>> synchronized on the
>>>> physical replica are not going to be valid.  Or if the primary drops 
>>>> its
>>>> logical slots.
>>>>
>>>>
>>>>> Should the redo function for the drop replication slot have the 
>>>>> capability
>>>>> to drop it on standby and its subscribers (if any) as well?
>>>>
>>>> Slots are not WAL logged (and shouldn't be).
>>>>
>>>> I think you pretty much need the recovery conflict handling 
>>>> infrastructure I
>>>> referenced upthread, which recognized during replay if a record has 
>>>> a conflict
>>>> with a slot on a standby.  And then ontop of that you can build 
>>>> something like
>>>> this patch.
>>>>
>>>
>>> OK. Understood, thanks Andres.
>>
>> I would love to see this feature in PG 15.  Can someone explain its
>> current status?  Thanks.
> 
> The way I understand it:
> 
> 1. This feature (probably) depends on the "Minimal logical decoding on 
> standbys" patch.  The details there aren't totally clear (to me).  That 
> patch had some activity lately but I don't see it in a state that it's 
> nearing readiness.
> 

FWIW, a proposal has been submitted in [1] to add information in the WAL 
records in preparation for logical slot conflict handling.

[1]: 
https://www.postgresql.org/message-id/[email protected]

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-12 12:19  shveta malik <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-12 12:19 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Fri, Jan 12, 2024 at 5:30 PM Masahiko Sawada <[email protected]> wrote:
>
> On Thu, Jan 11, 2024 at 7:53 PM Amit Kapila <[email protected]> wrote:
> >
> > On Tue, Jan 9, 2024 at 6:39 PM Amit Kapila <[email protected]> wrote:
> > >
> > > +static bool
> > > +synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
> > > {
> > > ...
> > > + /* Slot ready for sync, so sync it. */
> > > + else
> > > + {
> > > + /*
> > > + * Sanity check: With hot_standby_feedback enabled and
> > > + * invalidations handled appropriately as above, this should never
> > > + * happen.
> > > + */
> > > + if (remote_slot->restart_lsn < slot->data.restart_lsn)
> > > + elog(ERROR,
> > > + "cannot synchronize local slot \"%s\" LSN(%X/%X)"
> > > + " to remote slot's LSN(%X/%X) as synchronization"
> > > + " would move it backwards", remote_slot->name,
> > > + LSN_FORMAT_ARGS(slot->data.restart_lsn),
> > > + LSN_FORMAT_ARGS(remote_slot->restart_lsn));
> > > ...
> > > }
> > >
> > > I was thinking about the above code in the patch and as far as I can
> > > think this can only occur if the same name slot is re-created with
> > > prior restart_lsn after the existing slot is dropped. Normally, the
> > > newly created slot (with the same name) will have higher restart_lsn
> > > but one can mimic it by copying some older slot by using
> > > pg_copy_logical_replication_slot().
> > >
> > > I don't think as mentioned in comments even if hot_standby_feedback is
> > > temporarily set to off, the above shouldn't happen. It can only lead
> > > to invalidated slots on standby.
> > >
> > > To close the above race, I could think of the following ways:
> > > 1. Drop and re-create the slot.
> > > 2. Emit LOG/WARNING in this case and once remote_slot's LSN moves
> > > ahead of local_slot's LSN then we can update it; but as mentioned in
> > > your previous comment, we need to update all other fields as well. If
> > > we follow this then we probably need to have a check for catalog_xmin
> > > as well.
> > >
> >
> > The second point as mentioned is slightly misleading, so let me try to
> > rephrase it once again: Emit LOG/WARNING in this case and once
> > remote_slot's LSN moves ahead of local_slot's LSN then we can update
> > it; additionally, we need to update all other fields like two_phase as
> > well. If we follow this then we probably need to have a check for
> > catalog_xmin as well along remote_slot's restart_lsn.
> >
> > > Now, related to this the other case which needs some handling is what
> > > if the remote_slot's restart_lsn is greater than local_slot's
> > > restart_lsn but it is a re-created slot with the same name. In that
> > > case, I think the other properties like 'two_phase', 'plugin' could be
> > > different. So, is simply copying those sufficient or do we need to do
> > > something else as well?
> > >
> >
> > Bertrand, Dilip, Sawada-San, and others, please share your opinion on
> > this problem as I think it is important to handle this race condition.
>
> Is there any good use case of copying a failover slot in the first
> place? If it's not a normal use case and we can probably live without
> it, why not always disable failover during the copy? FYI we always
> disable two_phase on copied slots. It seems to me that copying a
> failover slot could lead to problems, as long as we synchronize slots
> based on their names. IIUC without the copy, this pass should never
> happen.
>
> Regards,
>
> --
> Masahiko Sawada
> Amazon Web Services: https://aws.amazon.com


There are multiple approaches discussed and tried when it comes to
starting a slot-sync worker. I am summarizing all here:

 1) Make slotsync worker as an Auxiliary Process (like checkpointer,
walwriter, walreceiver etc). The benefit this approach provides is, it
can control begin and stop in a more flexible way as each auxiliary
process could have different checks before starting and can have
different stop conditions. But it needs code duplication for process
management(start, stop, crash handling, signals etc) and currently it
does not support db-connection smoothly (none of the auxiliary process
has one so far)

We attempted to make slot-sync worker as an auxiliary process and
faced some challenges. The slot sync worker needs db-connection and
thus needs InitPostgres(). But AuxiliaryProcessMain() and
InitPostgres() are not compatible as both invoke common functions and
end up setting many callbacks functions twice (with different args).
Also InitPostgres() does 'MyBackendId' initialization (which further
triggers some stuff) which is not needed for AuxiliaryProcess and so
on. And thus in order to make slot-sync worker as an auxiliary
process, we need something similar to InitPostgres (trimmed down
working version) which needs further detailed analysis.

2) Make slotsync worker as a 'special' process like AutoVacLauncher
which is neither an Auxiliary process nor a bgworker one. It allows
db-connection and also provides flexibility to have start and stop
conditions for a process. But it needs a lot of code-duplication
around start, stop, fork (windows, non-windows), crash-management and
stuff.  It also needs to do many process-initialization stuff by
itself (which is otherwise done internally by Aux and bgworker infra).
And I am not sure if we should be adding a new process as a 'special'
one when postgres already provides bgworker and Auxiliary process
infrastructure.

3) Make slotysnc worker a bgworker. Here we just need to register our
process as a bgworker (RegisterBackgroundWorker()) by providing a
relevant start_time and restart_time and then the process management
is well taken care of. It does not need any code-duplication and
allows db-connection smoothly in registered process. The only thing it
lacks is that it does not provide flexibility of having
start-condition which then makes us to have 'enable_syncslot' as
PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
feel enable_syncslot is something which will not be changed frequently
and with the benefits provided by bgworker infra, it seems a
reasonably good option to choose this approach.

4) Another option is to have Logical Replication Launcher(or a new
process) to launch slot-sync worker. But going by the current design
where we have only 1 slotsync worker, it may be an overhead to have an
additional manager process maintained. Especially if we go by 'Logical
Replication Launcher', some extra changes will be needed there. It
will need start_time change from BgWorkerStart_RecoveryFinished to
BgWorkerStart_ConsistentState (doable but wanted to mention the
change). And provided the fact that 'Logical Replication Launcher'
does not have db-connection currently, in future if slotsync
validation-checks need to execute some sql query, it cannot do it
simply. It will either need the launcher to have db-connection or will
need new commands to be implemented for the same.

Thus weighing pros and cons of all these options, we have currently
implemented the bgworker approach (approach 3).  Any feedback is
welcome.

Thanks
Shveta






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-13 07:23  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 3 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-13 07:23 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
>
> There are multiple approaches discussed and tried when it comes to
> starting a slot-sync worker. I am summarizing all here:
>
>  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> walwriter, walreceiver etc). The benefit this approach provides is, it
> can control begin and stop in a more flexible way as each auxiliary
> process could have different checks before starting and can have
> different stop conditions. But it needs code duplication for process
> management(start, stop, crash handling, signals etc) and currently it
> does not support db-connection smoothly (none of the auxiliary process
> has one so far)
>

As slotsync worker needs to perform transactions and access syscache,
we can't make it an auxiliary process as that doesn't initialize the
required stuff like syscache. Also, see the comment "Auxiliary
processes don't run transactions ..." in AuxiliaryProcessMain() which
means this is not an option.

>
> 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> which is neither an Auxiliary process nor a bgworker one. It allows
> db-connection and also provides flexibility to have start and stop
> conditions for a process.
>

Yeah, due to these reasons, I think this option is worth considering
and another plus point is that this allows us to make enable_syncslot
a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.

>
> 3) Make slotysnc worker a bgworker. Here we just need to register our
> process as a bgworker (RegisterBackgroundWorker()) by providing a
> relevant start_time and restart_time and then the process management
> is well taken care of. It does not need any code-duplication and
> allows db-connection smoothly in registered process. The only thing it
> lacks is that it does not provide flexibility of having
> start-condition which then makes us to have 'enable_syncslot' as
> PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> feel enable_syncslot is something which will not be changed frequently
> and with the benefits provided by bgworker infra, it seems a
> reasonably good option to choose this approach.
>

I agree but it may be better to make it a PGC_SIGHUP parameter.

> 4) Another option is to have Logical Replication Launcher(or a new
> process) to launch slot-sync worker. But going by the current design
> where we have only 1 slotsync worker, it may be an overhead to have an
> additional manager process maintained.
>

I don't see any good reason to have an additional launcher process here.

>
> Thus weighing pros and cons of all these options, we have currently
> implemented the bgworker approach (approach 3).  Any feedback is
> welcome.
>

I vote to go for (2) unless we face difficulties in doing so but (3)
is also okay especially if others also think so.

-- 
With Regards,
Amit Kapila.






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 03:33  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  2 siblings, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-16 03:33 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
>
> On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> >
> > There are multiple approaches discussed and tried when it comes to
> > starting a slot-sync worker. I am summarizing all here:
> >
> >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > walwriter, walreceiver etc). The benefit this approach provides is, it
> > can control begin and stop in a more flexible way as each auxiliary
> > process could have different checks before starting and can have
> > different stop conditions. But it needs code duplication for process
> > management(start, stop, crash handling, signals etc) and currently it
> > does not support db-connection smoothly (none of the auxiliary process
> > has one so far)
> >
>
> As slotsync worker needs to perform transactions and access syscache,
> we can't make it an auxiliary process as that doesn't initialize the
> required stuff like syscache. Also, see the comment "Auxiliary
> processes don't run transactions ..." in AuxiliaryProcessMain() which
> means this is not an option.
>
> >
> > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > which is neither an Auxiliary process nor a bgworker one. It allows
> > db-connection and also provides flexibility to have start and stop
> > conditions for a process.
> >
>
> Yeah, due to these reasons, I think this option is worth considering
> and another plus point is that this allows us to make enable_syncslot
> a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
>
> >
> > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > relevant start_time and restart_time and then the process management
> > is well taken care of. It does not need any code-duplication and
> > allows db-connection smoothly in registered process. The only thing it
> > lacks is that it does not provide flexibility of having
> > start-condition which then makes us to have 'enable_syncslot' as
> > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > feel enable_syncslot is something which will not be changed frequently
> > and with the benefits provided by bgworker infra, it seems a
> > reasonably good option to choose this approach.
> >
>
> I agree but it may be better to make it a PGC_SIGHUP parameter.
>
> > 4) Another option is to have Logical Replication Launcher(or a new
> > process) to launch slot-sync worker. But going by the current design
> > where we have only 1 slotsync worker, it may be an overhead to have an
> > additional manager process maintained.
> >
>
> I don't see any good reason to have an additional launcher process here.
>
> >
> > Thus weighing pros and cons of all these options, we have currently
> > implemented the bgworker approach (approach 3).  Any feedback is
> > welcome.
> >
>
> I vote to go for (2) unless we face difficulties in doing so but (3)
> is also okay especially if others also think so.

I am not against any of the approaches but I still feel that when we
have a standard way of doing things (bgworker) we should not keep
adding code to do things in a special way unless there is a strong
reason to do so. Now we need to decide if 'enable_syncslot' being
PGC_POSTMASTER is a strong reason to go the non-standard way? If yes,
then we should think of option 2 else option 3 seems better in my
understanding (which may be limited due to my short experience here),
so I am all ears to what others think on this.

thanks
Shveta






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 04:07  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-16 04:07 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 16, 2024 at 9:03 AM shveta malik <[email protected]> wrote:
>
> On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
> >
> > On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> > >
> > > There are multiple approaches discussed and tried when it comes to
> > > starting a slot-sync worker. I am summarizing all here:
> > >
> > >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > > walwriter, walreceiver etc). The benefit this approach provides is, it
> > > can control begin and stop in a more flexible way as each auxiliary
> > > process could have different checks before starting and can have
> > > different stop conditions. But it needs code duplication for process
> > > management(start, stop, crash handling, signals etc) and currently it
> > > does not support db-connection smoothly (none of the auxiliary process
> > > has one so far)
> > >
> >
> > As slotsync worker needs to perform transactions and access syscache,
> > we can't make it an auxiliary process as that doesn't initialize the
> > required stuff like syscache. Also, see the comment "Auxiliary
> > processes don't run transactions ..." in AuxiliaryProcessMain() which
> > means this is not an option.
> >
> > >
> > > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > > which is neither an Auxiliary process nor a bgworker one. It allows
> > > db-connection and also provides flexibility to have start and stop
> > > conditions for a process.
> > >
> >
> > Yeah, due to these reasons, I think this option is worth considering
> > and another plus point is that this allows us to make enable_syncslot
> > a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
> >
> > >
> > > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > > relevant start_time and restart_time and then the process management
> > > is well taken care of. It does not need any code-duplication and
> > > allows db-connection smoothly in registered process. The only thing it
> > > lacks is that it does not provide flexibility of having
> > > start-condition which then makes us to have 'enable_syncslot' as
> > > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > > feel enable_syncslot is something which will not be changed frequently
> > > and with the benefits provided by bgworker infra, it seems a
> > > reasonably good option to choose this approach.
> > >
> >
> > I agree but it may be better to make it a PGC_SIGHUP parameter.
> >
> > > 4) Another option is to have Logical Replication Launcher(or a new
> > > process) to launch slot-sync worker. But going by the current design
> > > where we have only 1 slotsync worker, it may be an overhead to have an
> > > additional manager process maintained.
> > >
> >
> > I don't see any good reason to have an additional launcher process here.
> >
> > >
> > > Thus weighing pros and cons of all these options, we have currently
> > > implemented the bgworker approach (approach 3).  Any feedback is
> > > welcome.
> > >
> >
> > I vote to go for (2) unless we face difficulties in doing so but (3)
> > is also okay especially if others also think so.
>
> I am not against any of the approaches but I still feel that when we
> have a standard way of doing things (bgworker) we should not keep
> adding code to do things in a special way unless there is a strong
> reason to do so. Now we need to decide if 'enable_syncslot' being
> PGC_POSTMASTER is a strong reason to go the non-standard way?
>

Agreed and as said earlier I think it is better to make it a
PGC_SIGHUP. Also, not sure we can say it is a non-standard way as
already autovacuum launcher is handled in the same way. One more minor
thing is it will save us for having a new bgworker state
BgWorkerStart_ConsistentState_HotStandby as introduced by this patch.

> If yes,
> then we should think of option 2 else option 3 seems better in my
> understanding (which may be limited due to my short experience here),
> so I am all ears to what others think on this.
>

I also think it would be better if more people share their opinion on
this matter.

-- 
With Regards,
Amit Kapila.






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 04:33  Dilip Kumar <[email protected]>
  parent: Amit Kapila <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Dilip Kumar @ 2024-01-16 04:33 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 16, 2024 at 9:37 AM Amit Kapila <[email protected]> wrote:
>
> On Tue, Jan 16, 2024 at 9:03 AM shveta malik <[email protected]> wrote:
> >
> Agreed and as said earlier I think it is better to make it a
> PGC_SIGHUP. Also, not sure we can say it is a non-standard way as
> already autovacuum launcher is handled in the same way. One more minor
> thing is it will save us for having a new bgworker state
> BgWorkerStart_ConsistentState_HotStandby as introduced by this patch.

Yeah, it's not a nonstandard way.  But bgworker provides a lot of
inbuilt infrastructure which otherwise we would have to maintain by
ourselves if we opt for option 2.  I would have preferred option 3
from the simplicity point of view but I would prefer to make this
PGC_SIGHUP over simplicity.  But anyway, if there are issues in doing
so then we can keep it PGC_POSTMASTER but it's worth trying this out.

-- 
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 07:28  Masahiko Sawada <[email protected]>
  parent: Amit Kapila <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-16 07:28 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 16, 2024 at 1:07 PM Amit Kapila <[email protected]> wrote:
>
> On Tue, Jan 16, 2024 at 9:03 AM shveta malik <[email protected]> wrote:
> >
> > On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
> > >
> > > On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> > > >
> > > > There are multiple approaches discussed and tried when it comes to
> > > > starting a slot-sync worker. I am summarizing all here:
> > > >
> > > >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > > > walwriter, walreceiver etc). The benefit this approach provides is, it
> > > > can control begin and stop in a more flexible way as each auxiliary
> > > > process could have different checks before starting and can have
> > > > different stop conditions. But it needs code duplication for process
> > > > management(start, stop, crash handling, signals etc) and currently it
> > > > does not support db-connection smoothly (none of the auxiliary process
> > > > has one so far)
> > > >
> > >
> > > As slotsync worker needs to perform transactions and access syscache,
> > > we can't make it an auxiliary process as that doesn't initialize the
> > > required stuff like syscache. Also, see the comment "Auxiliary
> > > processes don't run transactions ..." in AuxiliaryProcessMain() which
> > > means this is not an option.
> > >
> > > >
> > > > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > > > which is neither an Auxiliary process nor a bgworker one. It allows
> > > > db-connection and also provides flexibility to have start and stop
> > > > conditions for a process.
> > > >
> > >
> > > Yeah, due to these reasons, I think this option is worth considering
> > > and another plus point is that this allows us to make enable_syncslot
> > > a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
> > >
> > > >
> > > > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > > > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > > > relevant start_time and restart_time and then the process management
> > > > is well taken care of. It does not need any code-duplication and
> > > > allows db-connection smoothly in registered process. The only thing it
> > > > lacks is that it does not provide flexibility of having
> > > > start-condition which then makes us to have 'enable_syncslot' as
> > > > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > > > feel enable_syncslot is something which will not be changed frequently
> > > > and with the benefits provided by bgworker infra, it seems a
> > > > reasonably good option to choose this approach.
> > > >
> > >
> > > I agree but it may be better to make it a PGC_SIGHUP parameter.
> > >
> > > > 4) Another option is to have Logical Replication Launcher(or a new
> > > > process) to launch slot-sync worker. But going by the current design
> > > > where we have only 1 slotsync worker, it may be an overhead to have an
> > > > additional manager process maintained.
> > > >
> > >
> > > I don't see any good reason to have an additional launcher process here.
> > >
> > > >
> > > > Thus weighing pros and cons of all these options, we have currently
> > > > implemented the bgworker approach (approach 3).  Any feedback is
> > > > welcome.
> > > >
> > >
> > > I vote to go for (2) unless we face difficulties in doing so but (3)
> > > is also okay especially if others also think so.
> >
> > I am not against any of the approaches but I still feel that when we
> > have a standard way of doing things (bgworker) we should not keep
> > adding code to do things in a special way unless there is a strong
> > reason to do so. Now we need to decide if 'enable_syncslot' being
> > PGC_POSTMASTER is a strong reason to go the non-standard way?
> >
>
> Agreed and as said earlier I think it is better to make it a
> PGC_SIGHUP. Also, not sure we can say it is a non-standard way as
> already autovacuum launcher is handled in the same way. One more minor
> thing is it will save us for having a new bgworker state
> BgWorkerStart_ConsistentState_HotStandby as introduced by this patch.

Why do we need to add a new BgWorkerStart_ConsistentState_HotStandby
for the slotsync worker? Isn't it sufficient that the slotsync worker
exits if not in hot standby mode?

Is there any technical difficulty or obstacle to make the slotsync
worker start using bgworker after reloading the config file?

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 08:42  Bertrand Drouvot <[email protected]>
  parent: Amit Kapila <[email protected]>
  2 siblings, 0 replies; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-16 08:42 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Hi,

On Sat, Jan 13, 2024 at 12:53:50PM +0530, Amit Kapila wrote:
> On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> >
> > There are multiple approaches discussed and tried when it comes to
> > starting a slot-sync worker. I am summarizing all here:
> >
> >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > walwriter, walreceiver etc). The benefit this approach provides is, it
> > can control begin and stop in a more flexible way as each auxiliary
> > process could have different checks before starting and can have
> > different stop conditions. But it needs code duplication for process
> > management(start, stop, crash handling, signals etc) and currently it
> > does not support db-connection smoothly (none of the auxiliary process
> > has one so far)
> >
> 
> As slotsync worker needs to perform transactions and access syscache,
> we can't make it an auxiliary process as that doesn't initialize the
> required stuff like syscache. Also, see the comment "Auxiliary
> processes don't run transactions ..." in AuxiliaryProcessMain() which
> means this is not an option.
> 
> >
> > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > which is neither an Auxiliary process nor a bgworker one. It allows
> > db-connection and also provides flexibility to have start and stop
> > conditions for a process.
> >
> 
> Yeah, due to these reasons, I think this option is worth considering
> and another plus point is that this allows us to make enable_syncslot
> a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
> 
> >
> > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > relevant start_time and restart_time and then the process management
> > is well taken care of. It does not need any code-duplication and
> > allows db-connection smoothly in registered process. The only thing it
> > lacks is that it does not provide flexibility of having
> > start-condition which then makes us to have 'enable_syncslot' as
> > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > feel enable_syncslot is something which will not be changed frequently
> > and with the benefits provided by bgworker infra, it seems a
> > reasonably good option to choose this approach.
> >
> 
> I agree but it may be better to make it a PGC_SIGHUP parameter.
> 
> > 4) Another option is to have Logical Replication Launcher(or a new
> > process) to launch slot-sync worker. But going by the current design
> > where we have only 1 slotsync worker, it may be an overhead to have an
> > additional manager process maintained.
> >
> 
> I don't see any good reason to have an additional launcher process here.
> 
> >
> > Thus weighing pros and cons of all these options, we have currently
> > implemented the bgworker approach (approach 3).  Any feedback is
> > welcome.
> >
> 
> I vote to go for (2) unless we face difficulties in doing so but (3)
> is also okay especially if others also think so.
> 

Yeah, I think that (2) would be the "ideal" one but (3) is fine too. I think
that if we think/see that (2) is too "complicated"/long to implement maybe we
could do (3) initially and switch to (2) later. What I mean by that is that I
don't think that not doing (2) should be a blocker.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 09:40  shveta malik <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: shveta malik @ 2024-01-16 09:40 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 16, 2024 at 12:59 PM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Jan 16, 2024 at 1:07 PM Amit Kapila <[email protected]> wrote:
> >
> > On Tue, Jan 16, 2024 at 9:03 AM shveta malik <[email protected]> wrote:
> > >
> > > On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
> > > >
> > > > On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> > > > >
> > > > > There are multiple approaches discussed and tried when it comes to
> > > > > starting a slot-sync worker. I am summarizing all here:
> > > > >
> > > > >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > > > > walwriter, walreceiver etc). The benefit this approach provides is, it
> > > > > can control begin and stop in a more flexible way as each auxiliary
> > > > > process could have different checks before starting and can have
> > > > > different stop conditions. But it needs code duplication for process
> > > > > management(start, stop, crash handling, signals etc) and currently it
> > > > > does not support db-connection smoothly (none of the auxiliary process
> > > > > has one so far)
> > > > >
> > > >
> > > > As slotsync worker needs to perform transactions and access syscache,
> > > > we can't make it an auxiliary process as that doesn't initialize the
> > > > required stuff like syscache. Also, see the comment "Auxiliary
> > > > processes don't run transactions ..." in AuxiliaryProcessMain() which
> > > > means this is not an option.
> > > >
> > > > >
> > > > > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > > > > which is neither an Auxiliary process nor a bgworker one. It allows
> > > > > db-connection and also provides flexibility to have start and stop
> > > > > conditions for a process.
> > > > >
> > > >
> > > > Yeah, due to these reasons, I think this option is worth considering
> > > > and another plus point is that this allows us to make enable_syncslot
> > > > a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
> > > >
> > > > >
> > > > > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > > > > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > > > > relevant start_time and restart_time and then the process management
> > > > > is well taken care of. It does not need any code-duplication and
> > > > > allows db-connection smoothly in registered process. The only thing it
> > > > > lacks is that it does not provide flexibility of having
> > > > > start-condition which then makes us to have 'enable_syncslot' as
> > > > > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > > > > feel enable_syncslot is something which will not be changed frequently
> > > > > and with the benefits provided by bgworker infra, it seems a
> > > > > reasonably good option to choose this approach.
> > > > >
> > > >
> > > > I agree but it may be better to make it a PGC_SIGHUP parameter.
> > > >
> > > > > 4) Another option is to have Logical Replication Launcher(or a new
> > > > > process) to launch slot-sync worker. But going by the current design
> > > > > where we have only 1 slotsync worker, it may be an overhead to have an
> > > > > additional manager process maintained.
> > > > >
> > > >
> > > > I don't see any good reason to have an additional launcher process here.
> > > >
> > > > >
> > > > > Thus weighing pros and cons of all these options, we have currently
> > > > > implemented the bgworker approach (approach 3).  Any feedback is
> > > > > welcome.
> > > > >
> > > >
> > > > I vote to go for (2) unless we face difficulties in doing so but (3)
> > > > is also okay especially if others also think so.
> > >
> > > I am not against any of the approaches but I still feel that when we
> > > have a standard way of doing things (bgworker) we should not keep
> > > adding code to do things in a special way unless there is a strong
> > > reason to do so. Now we need to decide if 'enable_syncslot' being
> > > PGC_POSTMASTER is a strong reason to go the non-standard way?
> > >
> >
> > Agreed and as said earlier I think it is better to make it a
> > PGC_SIGHUP. Also, not sure we can say it is a non-standard way as
> > already autovacuum launcher is handled in the same way. One more minor
> > thing is it will save us for having a new bgworker state
> > BgWorkerStart_ConsistentState_HotStandby as introduced by this patch.
>
> Why do we need to add a new BgWorkerStart_ConsistentState_HotStandby
> for the slotsync worker? Isn't it sufficient that the slotsync worker
> exits if not in hot standby mode?

It is doable, but that will mean starting slot-sync worker even on
primary on every server restart which does not seem like a good idea.
We wanted to have a way where-in it does not start itself in
non-standby mode.

> Is there any technical difficulty or obstacle to make the slotsync
> worker start using bgworker after reloading the config file?

When we register slotsync worker as bgworker, we can only register the
bgworker before initializing shared memory, we cannot register
dynamically in the cycle of ServerLoop and thus we do not have
flexibility of registering/deregistering the bgworker  (or controlling
the bgworker start) based on config parameters each time they change.
We can always start slot-sync worker and let it check if
enable_syncslot is ON. If not, exit and retry the next time when
postmaster will restart it after restart_time(60sec). The downside of
this approach is, even if any user does not want slot-sync
functionality and thus has permanently disabled 'enable_syncslot', it
will keep on restarting and exiting there.


thanks
Shveta






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 11:57  shveta malik <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 4 replies; 119+ messages in thread

From: shveta malik @ 2024-01-16 11:57 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 16, 2024 at 3:10 PM shveta malik <[email protected]> wrote:
>
> On Tue, Jan 16, 2024 at 12:59 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Jan 16, 2024 at 1:07 PM Amit Kapila <[email protected]> wrote:
> > >
> > > On Tue, Jan 16, 2024 at 9:03 AM shveta malik <[email protected]> wrote:
> > > >
> > > > On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
> > > > >
> > > > > On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> > > > > >
> > > > > > There are multiple approaches discussed and tried when it comes to
> > > > > > starting a slot-sync worker. I am summarizing all here:
> > > > > >
> > > > > >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > > > > > walwriter, walreceiver etc). The benefit this approach provides is, it
> > > > > > can control begin and stop in a more flexible way as each auxiliary
> > > > > > process could have different checks before starting and can have
> > > > > > different stop conditions. But it needs code duplication for process
> > > > > > management(start, stop, crash handling, signals etc) and currently it
> > > > > > does not support db-connection smoothly (none of the auxiliary process
> > > > > > has one so far)
> > > > > >
> > > > >
> > > > > As slotsync worker needs to perform transactions and access syscache,
> > > > > we can't make it an auxiliary process as that doesn't initialize the
> > > > > required stuff like syscache. Also, see the comment "Auxiliary
> > > > > processes don't run transactions ..." in AuxiliaryProcessMain() which
> > > > > means this is not an option.
> > > > >
> > > > > >
> > > > > > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > > > > > which is neither an Auxiliary process nor a bgworker one. It allows
> > > > > > db-connection and also provides flexibility to have start and stop
> > > > > > conditions for a process.
> > > > > >
> > > > >
> > > > > Yeah, due to these reasons, I think this option is worth considering
> > > > > and another plus point is that this allows us to make enable_syncslot
> > > > > a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
> > > > >
> > > > > >
> > > > > > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > > > > > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > > > > > relevant start_time and restart_time and then the process management
> > > > > > is well taken care of. It does not need any code-duplication and
> > > > > > allows db-connection smoothly in registered process. The only thing it
> > > > > > lacks is that it does not provide flexibility of having
> > > > > > start-condition which then makes us to have 'enable_syncslot' as
> > > > > > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > > > > > feel enable_syncslot is something which will not be changed frequently
> > > > > > and with the benefits provided by bgworker infra, it seems a
> > > > > > reasonably good option to choose this approach.
> > > > > >
> > > > >
> > > > > I agree but it may be better to make it a PGC_SIGHUP parameter.
> > > > >
> > > > > > 4) Another option is to have Logical Replication Launcher(or a new
> > > > > > process) to launch slot-sync worker. But going by the current design
> > > > > > where we have only 1 slotsync worker, it may be an overhead to have an
> > > > > > additional manager process maintained.
> > > > > >
> > > > >
> > > > > I don't see any good reason to have an additional launcher process here.
> > > > >
> > > > > >
> > > > > > Thus weighing pros and cons of all these options, we have currently
> > > > > > implemented the bgworker approach (approach 3).  Any feedback is
> > > > > > welcome.
> > > > > >
> > > > >
> > > > > I vote to go for (2) unless we face difficulties in doing so but (3)
> > > > > is also okay especially if others also think so.
> > > >
> > > > I am not against any of the approaches but I still feel that when we
> > > > have a standard way of doing things (bgworker) we should not keep
> > > > adding code to do things in a special way unless there is a strong
> > > > reason to do so. Now we need to decide if 'enable_syncslot' being
> > > > PGC_POSTMASTER is a strong reason to go the non-standard way?
> > > >
> > >
> > > Agreed and as said earlier I think it is better to make it a
> > > PGC_SIGHUP. Also, not sure we can say it is a non-standard way as
> > > already autovacuum launcher is handled in the same way. One more minor
> > > thing is it will save us for having a new bgworker state
> > > BgWorkerStart_ConsistentState_HotStandby as introduced by this patch.
> >
> > Why do we need to add a new BgWorkerStart_ConsistentState_HotStandby
> > for the slotsync worker? Isn't it sufficient that the slotsync worker
> > exits if not in hot standby mode?
>
> It is doable, but that will mean starting slot-sync worker even on
> primary on every server restart which does not seem like a good idea.
> We wanted to have a way where-in it does not start itself in
> non-standby mode.
>
> > Is there any technical difficulty or obstacle to make the slotsync
> > worker start using bgworker after reloading the config file?
>
> When we register slotsync worker as bgworker, we can only register the
> bgworker before initializing shared memory, we cannot register
> dynamically in the cycle of ServerLoop and thus we do not have
> flexibility of registering/deregistering the bgworker  (or controlling
> the bgworker start) based on config parameters each time they change.
> We can always start slot-sync worker and let it check if
> enable_syncslot is ON. If not, exit and retry the next time when
> postmaster will restart it after restart_time(60sec). The downside of
> this approach is, even if any user does not want slot-sync
> functionality and thus has permanently disabled 'enable_syncslot', it
> will keep on restarting and exiting there.


PFA v62. Details:

v62-001: No change.

v62-002:
1) Addressed slotsync.c related comments by Peter in [1].
2) Addressed CFBot failure where there was a crash in 32 bit env while
accessing DatumGetLSN
3) Addressed another CFBot failure where the test for
'050_standby_failover_slots_sync.pl' was hanging. Thanks Hou-San for
this fix.

v62-003:
It is a new patch which attempts to implement slot-sync worker as a
special process which is neither a bgworker nor an Auxiliary process.
Here we get the benefit of converting enable_syncslot to a PGC_SIGHUP
Guc rather than PGC_POSTMASTER. We launch the slot-sync worker only if
it is hot-standby and 'enable_syncslot' is ON.

v62-004:
Small change in document.

v62-005: No change

v62-006:
Separated the failover-ready validation steps into this separate
doc-patch (which were earlier present in v61-002 and v61-003). Also
addressed some of the doc comments by Peter in [1].
Thanks Hou-San for providing this patch.

[1]: https://www.postgresql.org/message-id/CAHut%2BPteZVNx1jQ6Hs3mEdoC%3DDNALVpJJ2mZDYim7sU-04tiaw%40mail...

thanks
Shveta


Attachments:

  [application/octet-stream] v62-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.3K, ../../CAJpy0uCJ7MKgZnKLAACd-AXN0VbWM5gVJ+GRJ1Za_A2UmF3R0A@mail.gmail.com/2-v62-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 190cb66414cbba8a8b922807e85708292cb85b26 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 16:08:07 +0530
Subject: [PATCH v62 4/6] Allow logical walsenders to wait for the physical
 standbys

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   4 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 733 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index 6721d8951d..edb511c065 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -355,7 +355,9 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      be enabled on the standby. It's also highly recommended that the said
      physical replication slot is named in <varname>standby_slot_names</varname>
      list on the primary, to prevent the subscriber from consuming changes
-     faster than the hot standby.
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in <varname>standby_slot_names</varname>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 33f957b02f..d0509fb5a5 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2210,3 +2224,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index c81a7a8344..acf562fe93 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3527,6 +3599,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3596,8 +3669,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0879bab57e..fead31748e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5763454336..08db4c37bc 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4618,6 +4618,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 6830f7d16b..0c7b6117e0 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f81bef9e42..eef9f25c45 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..9a42b01f9a 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -45,6 +45,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index c17abfb40b..d50bbb3ae7 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v62-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uCJ7MKgZnKLAACd-AXN0VbWM5gVJ+GRJ1Za_A2UmF3R0A@mail.gmail.com/3-v62-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 996c0c19c306d62f177c148e814e89594c7c2761 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 16:22:05 +0530
Subject: [PATCH v62 5/6] Non replication connection and app_name change.

Changes in this patch:
1) Convert replication connection to non-replication one in slotsync worker.
2) Use app_name as {cluster_name}_slotsyncworker in the slotsync worker
connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index f50be29d99..13da6b1466 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1530,7 +1532,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1781,7 +1784,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 934c1a3ab0..d6f0cae0a9 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -50,7 +53,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -125,7 +129,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -136,8 +145,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	const char *keys[6];
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 5dbb8ab4de..8d9b1cc4cb 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1049,6 +1049,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1150,13 +1151,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 3dea10f9b3..95e7324c8f 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index ffacd55e5c..cfe647ec58 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 5e942cb4fc..68ac074274 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -434,8 +435,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v62-0002-Add-logical-slot-sync-capability-to-the-physical.patch (82.7K, ../../CAJpy0uCJ7MKgZnKLAACd-AXN0VbWM5gVJ+GRJ1Za_A2UmF3R0A@mail.gmail.com/4-v62-0002-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 4b291ac9611ffa5993bc15f7ceb82a4b6f91d69f Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Fri, 12 Jan 2024 20:41:19 +0800
Subject: [PATCH v62 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   33 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1158 +++++++++++++++++
 src/backend/replication/slot.c                |   32 +-
 src/backend/replication/slotfuncs.c           |   14 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |    4 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    2 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   19 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 33 files changed, 1676 insertions(+), 37 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..6721d8951d 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -346,6 +346,39 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      <function>pg_log_standby_snapshot</function> function on the primary.
     </para>
 
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the failover option during slot creation and setting
+     <xref linkend="guc-enable-syncslot"/> on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and <varname>hot_standby_feedback</varname> must
+     be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in <varname>standby_slot_names</varname>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
+
     <caution>
      <para>
       Replication slots persist across crashes and know nothing about the state
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1868b95836..4f36a2f732 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index c5232cee2f..934c1a3ab0 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -34,6 +34,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -100,6 +102,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -432,6 +435,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..87d4f5e8b1
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1158 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by startup process in order to
+ * shut it down during promotion.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * Sleep time in ms between slot-sync cycles.
+ * See wait_for_slot_activity() for how we adjust this
+ */
+static long sleep_ms;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	dbid = get_database_oid(remote_slot->database, false);
+
+	if (namestrcmp(&slot->data.plugin, remote_slot->plugin) == 0 &&
+		slot->data.database == dbid &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	SpinLockAcquire(&slot->mutex);
+	namestrcpy(&slot->data.plugin, remote_slot->plugin);
+	slot->data.database = dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true either if the slot is marked as RS_PERSISTENT (sync-ready) or
+ * is synced periodically (if it was already sync-ready). Return false
+ * otherwise.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	(void) local_slot_update(remote_slot);
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly locally created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	if (remote_slot->confirmed_lsn > latestWalEnd)
+	{
+		elog(ERROR, "exiting from slot synchronization as the received slot sync"
+			 " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+			 LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+			 remote_slot->name,
+			 LSN_FORMAT_ARGS(latestWalEnd));
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization on receiving"
+						   " the failover slot \"%s\" from the primary server",
+						   remote_slot->name),
+					errdetail("A user-created slot with the same name already"
+							  " exists on the standby."));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(remote_slot->database, false);
+		namestrcpy(&slot->data.plugin, remote_slot->plugin);
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info"
+					   " from the primary server: %s", res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, 7, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 8, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 9, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the"
+					   " primary server: %s", PrimarySlotName, res->err));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	(void) tuplestore_gettupleslot(res->tuplestore, true, false, tupslot);
+
+	/* It must return one tuple */
+	Assert(tuplestore_tuple_count(res->tuplestore) == 1);
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("wal_level must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of"
+					   " a parameter change"));
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down"
+					   " on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for logical replication launcher.
+ *
+ * Called on logical replication launcher exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the logical replication launcher, or when there
+	 * is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("enable_syncslot is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 696376400e..33f957b02f 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to physical standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -315,6 +316,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +682,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +711,16 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	/*
+	 * Do not allow users to alter the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot alter replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +733,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +889,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index c93dba855b..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..c81a7a8344 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index e5119ed55d..04fed1007e 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
@@ -342,6 +343,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1eaaf3c6c5..19b08c1b5f 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index f625473ad4..0879bab57e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,8 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_PRIMARY_CATCHUP	"Waiting for the primary to catch-up, in slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index e53ebc6dc2..0f5ec63de1 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2044,6 +2045,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 835b0e9ba8..6830f7d16b 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b9e747d72f..6dc234f9f7 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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 585ccbb504..f81bef9e42 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for physical standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..5e942cb4fc 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,21 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbinfo_for_failover_slots_fn
+ *
+ * Run LIST_DBID_FOR_FAILOVER_SLOTS on primary server to get the
+ * list of unique DBIDs for failover logical slots
+ */
+typedef List *(*walrcv_get_dbinfo_for_failover_slots_fn) (WalReceiverConn *conn);
+
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +418,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +444,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +503,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..c17abfb40b 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly locally created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT failover, synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t|t",
+	'logical slot has failover as true and synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 271313ebf8..aac83755de 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -132,8 +132,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(22 rows)
+(23 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3d2559feca..9c83c320c8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2321,6 +2321,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2581,6 +2582,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v62-0001-Enable-setting-failover-property-for-a-slot-thro.patch (100.3K, ../../CAJpy0uCJ7MKgZnKLAACd-AXN0VbWM5gVJ+GRJ1Za_A2UmF3R0A@mail.gmail.com/5-v62-0001-Enable-setting-failover-property-for-a-slot-thro.patch)
  download | inline diff:
From 7f4bf353e7b687e1d24073b2201bb01cac6b105b Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 4 Jan 2024 09:15:26 +0530
Subject: [PATCH v62 1/6] Enable setting failover property for a slot through
 SQL API and subscription commands

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the next patch will
address that.

In addition, a new replication command named ALTER_REPLICATION_SLOT and a
corresponding walreceiver API function named walrcv_alter_slot have been
implemented. These additions provide subscribers or users the ability to
modify the failover property of a replication slot on the publisher.

Moreover, a new subscription option called 'failover' has been added,
allowing users to set it when creating or altering a subscription. Also,
a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out       |  58 ++++++
 contrib/test_decoding/sql/slot.sql            |  13 ++
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/func.sgml                        |  11 +-
 doc/src/sgml/protocol.sgml                    |  52 ++++++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 doc/src/sgml/system-views.sgml                |  11 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_functions.sql      |   1 +
 src/backend/catalog/system_views.sql          |   6 +-
 src/backend/commands/subscriptioncmds.c       | 105 ++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  38 +++-
 src/backend/replication/logical/tablesync.c   |   1 +
 src/backend/replication/logical/worker.c      |   7 +
 src/backend/replication/repl_gram.y           |  20 ++-
 src/backend/replication/repl_scanner.l        |   2 +
 src/backend/replication/slot.c                |  33 +++-
 src/backend/replication/slotfuncs.c           |  22 ++-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |  64 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/info.c                     |   5 +-
 src/bin/pg_upgrade/pg_upgrade.c               |   6 +-
 src/bin/pg_upgrade/pg_upgrade.h               |   2 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_proc.dat               |  14 +-
 src/include/catalog/pg_subscription.h         |  11 ++
 src/include/nodes/replnodes.h                 |  12 ++
 src/include/replication/slot.h                |   9 +-
 src/include/replication/walreceiver.h         |  18 +-
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 39 files changed, 745 insertions(+), 124 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..f224327c00 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the physical standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..154c426df7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        physical standbys so that logical replication can be resumed
+        after failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..af997efd2b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..ce386a46a7 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the physical standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..1868b95836 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the physical
+       standbys so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
@@ -1357,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..f50be29d99 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,30 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set failover for a subscription that does not have a slot name")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified if
+					 * the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1512,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error
+	 * while doing the database operations we won't be able to rollback
+	 * altered slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 201c36cb22..c5232cee2f 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -74,8 +74,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -96,6 +99,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -899,8 +903,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -929,7 +933,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -998,6 +1003,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 911835c5cb..3dea10f9b3 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..696376400e 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to physical standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -311,6 +314,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -679,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, true);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..c93dba855b 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -133,7 +135,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +174,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +192,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +431,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -782,11 +789,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e00395ff2b..ffacd55e5c 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,13 +1217,14 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 09914165e4..6b9aa208c0 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3335,7 +3335,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 58811a6530..b9e747d72f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11115,17 +11115,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ca32625585..c92a81e6b7 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,12 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -145,6 +151,11 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..585ccbb504 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for physical standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,9 +224,10 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f582eb59e7..3d2559feca 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3874,6 +3875,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v62-0003-Slot-sync-worker-as-a-special-process.patch (36.3K, ../../CAJpy0uCJ7MKgZnKLAACd-AXN0VbWM5gVJ+GRJ1Za_A2UmF3R0A@mail.gmail.com/6-v62-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From e29ffcf9bac74f5212ebc99b76a0faddad87b3f7 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 15:28:37 +0530
Subject: [PATCH v62 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to 'enable_syncslot' as PGC_SIGHUP which was otherwise
a PGC_POSTMASTER GUC when slotsync worker was registered as bgworker.
---
 doc/src/sgml/bgworker.sgml                 |  65 +---
 src/backend/postmaster/bgworker.c          |   3 -
 src/backend/postmaster/postmaster.c        |  85 ++++--
 src/backend/replication/logical/slotsync.c | 329 ++++++++++++++++-----
 src/backend/storage/lmgr/proc.c            |   9 +-
 src/backend/tcop/postgres.c                |  11 -
 src/backend/utils/activity/pgstat_io.c     |   1 +
 src/backend/utils/init/miscinit.c          |   7 +-
 src/backend/utils/init/postinit.c          |   8 +-
 src/backend/utils/misc/guc_tables.c        |   2 +-
 src/include/miscadmin.h                    |   1 +
 src/include/postmaster/bgworker.h          |   1 -
 src/include/replication/worker_internal.h  |   7 +-
 13 files changed, 354 insertions(+), 175 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..2fc979b1b8 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,9 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY)
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1015,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1835,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2686,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3045,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3217,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sycn worker? Normal exit or FATAL exit (FATAL can
+		 * be caused by libpqwalreceiver on receiving shutdown request by the
+		 * startup process during promotion) can be ignored; we'll start a new
+		 * one at the next iteration of the postmaster's main loop, if
+		 * necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("Slotsync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3599,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3745,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3762,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher or bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3778,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3877,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4101,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4915,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5019,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5848,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 87d4f5e8b1..5dbb8ab4de 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -27,6 +27,8 @@
  * It waits for a period of time before the next synchronization, with the
  * duration varying based on whether any slots were updated during the last
  * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
  *---------------------------------------------------------------------------
  */
 
@@ -40,7 +42,9 @@
 #include "commands/dbcommands.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -53,6 +57,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -78,12 +84,17 @@ typedef struct RemoteSlot
  * Struct for sharing information between startup process and slot
  * sync worker.
  *
- * Slot sync worker's pid is needed by startup process in order to
- * shut it down during promotion.
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot slyc worker. If stopSignaled is set, the worker will
+ * exit in such a case.
  */
 typedef struct SlotSyncWorkerCtxStruct
 {
 	pid_t		pid;
+	bool		stopSignaled;
 	slock_t		mutex;
 } SlotSyncWorkerCtxStruct;
 
@@ -92,13 +103,25 @@ SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
 /* GUC variable */
 bool		enable_syncslot = false;
 
+/* Flag to tell if we are in an slot sync worker process */
+static bool am_slotsync_worker = false;
+
 /*
  * Sleep time in ms between slot-sync cycles.
  * See wait_for_slot_activity() for how we adjust this
  */
 static long sleep_ms;
 
+/* Min and Max sleep time for slot sync worker */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 
 /*
  * If necessary, update local slot metadata based on the data from the remote
@@ -683,7 +706,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -698,8 +722,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -737,12 +763,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("skipping slot synchronization due to bad configuration"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -752,18 +781,18 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 
 /*
  * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * appropriately. If not, log the message and pass 'valid' as false
+ * to the caller.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
  * returns it.
  */
 static char *
-validate_parameters_and_get_dbname(void)
+validate_parameters_and_get_dbname(bool *valid)
 {
 	char	   *dbname;
 
-	/* Sanity check. */
-	Assert(enable_syncslot);
+	*valid = false;
 
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
@@ -772,10 +801,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_slot_name"));
+		return NULL;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -783,30 +815,39 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be enabled.", "hot_standby_feedback"));
+		return NULL;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("wal_level must be >= logical."));
+		return NULL;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_conninfo"));
+		return NULL;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
@@ -814,14 +855,61 @@ validate_parameters_and_get_dbname(void)
 	 */
 	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
 	if (dbname == NULL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+		return NULL;
+	}
+
+	/* All good, set valid to true now */
+	*valid = true;
+
+	return dbname;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+	bool		valid;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		dbname = validate_parameters_and_get_dbname(&valid);
+		if (valid)
+			break;
+		else
+		{
+			ProcessSlotSyncInterrupts(NULL);
+
+			rc = WaitLatch(MyLatch,
+						   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+						   MAX_WORKER_NAPTIME_MS,
+						   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+			if (rc & WL_LATCH_SET)
+				ResetLatch(MyLatch);
+
+		}
+	}
 
 	return dbname;
 }
@@ -837,6 +925,7 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
@@ -849,13 +938,13 @@ slotsync_reread_config(void)
 
 	if (conninfo_changed ||
 		primary_slotname_changed ||
+		old_enable_syncslot != enable_syncslot ||
 		(old_hot_standby_feedback != hot_standby_feedback))
 	{
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of"
 					   " a parameter change"));
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -872,7 +961,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down"
 					   " on receiving SIGINT"));
@@ -905,20 +995,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
-#define MIN_WORKER_NAPTIME_MS  200
-#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
-
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -954,44 +1040,115 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
+
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
 
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	BackgroundWorkerUnblockSignals();
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1010,26 +1167,27 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1046,7 +1204,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1062,6 +1220,7 @@ ShutDownSlotSync(void)
 		return;
 	}
 
+	SlotSyncWorker->stopSignaled = true;
 	kill(SlotSyncWorker->pid, SIGINT);
 
 	SpinLockRelease(&SlotSyncWorker->mutex);
@@ -1117,42 +1276,64 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("enable_syncslot is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
+
+	Assert(ac < lengthof(av));
+
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	memset(&bgw, 0, sizeof(bgw));
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+#else
+	switch ((pid = fork_process()))
+#endif
+	{
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+#ifndef EXEC_BACKEND
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..ad1352bf76 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,11 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker does not participate in it, see comments atop Backend.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -935,7 +939,8 @@ ProcKill(int code, Datum arg)
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 19b08c1b5f..1eaaf3c6c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..c5de528b34 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -341,6 +341,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
+		case B_SLOTSYNC_WORKER:
 			return true;
 	}
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..d5e16f1df4 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -311,6 +312,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_WAL_WRITER:
 			backendDesc = "walwriter";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 	}
 
 	return backendDesc;
@@ -837,7 +841,8 @@ InitializeSessionUserIdStandalone(void)
 	 * This function should only be called in single-user mode, in autovacuum
 	 * workers, and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0f5ec63de1..5763454336 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2046,7 +2046,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..e89b8121f3 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -337,6 +337,7 @@ typedef enum BackendType
 	B_WAL_RECEIVER,
 	B_WAL_SENDER,
 	B_WAL_SUMMARIZER,
+	B_SLOTSYNC_WORKER,
 	B_WAL_WRITER,
 } BackendType;
 
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..cd530d3d40 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,10 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v62-0006-Document-the-steps-to-check-if-the-standby-is-re.patch (5.4K, ../../CAJpy0uCJ7MKgZnKLAACd-AXN0VbWM5gVJ+GRJ1Za_A2UmF3R0A@mail.gmail.com/7-v62-0006-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From d4cd6662b162d229e62250070dceda409ecaddae Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Tue, 16 Jan 2024 14:02:32 +0800
Subject: [PATCH v62 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/logical-replication.sgml | 125 ++++++++++++++++++++++++++
 1 file changed, 125 insertions(+)

diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..6539c60843 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,131 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use <varname>standby_slot_names</varname> to
+   prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify the
+       slot names that should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server. This step can be skipped if
+       <varname>standby_slot_names</varname> has been correctly configured.
+<programlisting>
+test_standby=# SELECT bool_and(synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name in ('sub');
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL. If the
+        query result is NULL, it indicates that the subscriber has not yet
+        replayed any WAL. Therefore, the next step can be skipped, as the
+        standby server must be ahead of the subscriber.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) as remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-16 12:02  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  2 siblings, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-16 12:02 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
>
> On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> >
> > There are multiple approaches discussed and tried when it comes to
> > starting a slot-sync worker. I am summarizing all here:
> >
> >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > walwriter, walreceiver etc). The benefit this approach provides is, it
> > can control begin and stop in a more flexible way as each auxiliary
> > process could have different checks before starting and can have
> > different stop conditions. But it needs code duplication for process
> > management(start, stop, crash handling, signals etc) and currently it
> > does not support db-connection smoothly (none of the auxiliary process
> > has one so far)
> >
>
> As slotsync worker needs to perform transactions and access syscache,
> we can't make it an auxiliary process as that doesn't initialize the
> required stuff like syscache. Also, see the comment "Auxiliary
> processes don't run transactions ..." in AuxiliaryProcessMain() which
> means this is not an option.
>
> >
> > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > which is neither an Auxiliary process nor a bgworker one. It allows
> > db-connection and also provides flexibility to have start and stop
> > conditions for a process.
> >
>
> Yeah, due to these reasons, I think this option is worth considering
> and another plus point is that this allows us to make enable_syncslot
> a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
>
> >
> > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > relevant start_time and restart_time and then the process management
> > is well taken care of. It does not need any code-duplication and
> > allows db-connection smoothly in registered process. The only thing it
> > lacks is that it does not provide flexibility of having
> > start-condition which then makes us to have 'enable_syncslot' as
> > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > feel enable_syncslot is something which will not be changed frequently
> > and with the benefits provided by bgworker infra, it seems a
> > reasonably good option to choose this approach.
> >
>
> I agree but it may be better to make it a PGC_SIGHUP parameter.
>
> > 4) Another option is to have Logical Replication Launcher(or a new
> > process) to launch slot-sync worker. But going by the current design
> > where we have only 1 slotsync worker, it may be an overhead to have an
> > additional manager process maintained.
> >
>
> I don't see any good reason to have an additional launcher process here.
>
> >
> > Thus weighing pros and cons of all these options, we have currently
> > implemented the bgworker approach (approach 3).  Any feedback is
> > welcome.
> >
>
> I vote to go for (2) unless we face difficulties in doing so but (3)
> is also okay especially if others also think so.

Okay. Attempted approach 2 as a separate patch in v62-0003. Approach 3
(bgworker) is still  maintained in v62-002.

thanks
Shveta






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 00:42  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  3 siblings, 0 replies; 119+ messages in thread

From: Peter Smith @ 2024-01-17 00:42 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

About v62-0001:

As stated in the patch comment:
But note that this commit does not yet include the capability to
actually sync the replication slot; the next patch will address that.

~~~

Because of this, I think it might be prudent to separate the
documentation portion from this patch so that it can be pushed later
when the actual synchronize capability also gets pushed.

It would not be good for the PG documentation on HEAD to be describing
behaviour that does not yet exist. (e.g. if patch 0001 is pushed
early, but then there is some delay or problems getting the subsequent
patches committed).

======
Kind Regards,
Peter Smith.
Fujitsu Australia






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 01:13  Masahiko Sawada <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 2 replies; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-17 01:13 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Tue, Jan 16, 2024 at 6:40 PM shveta malik <[email protected]> wrote:
>
> On Tue, Jan 16, 2024 at 12:59 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Jan 16, 2024 at 1:07 PM Amit Kapila <[email protected]> wrote:
> > >
> > > On Tue, Jan 16, 2024 at 9:03 AM shveta malik <[email protected]> wrote:
> > > >
> > > > On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
> > > > >
> > > > > On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> > > > > >
> > > > > > There are multiple approaches discussed and tried when it comes to
> > > > > > starting a slot-sync worker. I am summarizing all here:
> > > > > >
> > > > > >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > > > > > walwriter, walreceiver etc). The benefit this approach provides is, it
> > > > > > can control begin and stop in a more flexible way as each auxiliary
> > > > > > process could have different checks before starting and can have
> > > > > > different stop conditions. But it needs code duplication for process
> > > > > > management(start, stop, crash handling, signals etc) and currently it
> > > > > > does not support db-connection smoothly (none of the auxiliary process
> > > > > > has one so far)
> > > > > >
> > > > >
> > > > > As slotsync worker needs to perform transactions and access syscache,
> > > > > we can't make it an auxiliary process as that doesn't initialize the
> > > > > required stuff like syscache. Also, see the comment "Auxiliary
> > > > > processes don't run transactions ..." in AuxiliaryProcessMain() which
> > > > > means this is not an option.
> > > > >
> > > > > >
> > > > > > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > > > > > which is neither an Auxiliary process nor a bgworker one. It allows
> > > > > > db-connection and also provides flexibility to have start and stop
> > > > > > conditions for a process.
> > > > > >
> > > > >
> > > > > Yeah, due to these reasons, I think this option is worth considering
> > > > > and another plus point is that this allows us to make enable_syncslot
> > > > > a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
> > > > >
> > > > > >
> > > > > > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > > > > > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > > > > > relevant start_time and restart_time and then the process management
> > > > > > is well taken care of. It does not need any code-duplication and
> > > > > > allows db-connection smoothly in registered process. The only thing it
> > > > > > lacks is that it does not provide flexibility of having
> > > > > > start-condition which then makes us to have 'enable_syncslot' as
> > > > > > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > > > > > feel enable_syncslot is something which will not be changed frequently
> > > > > > and with the benefits provided by bgworker infra, it seems a
> > > > > > reasonably good option to choose this approach.
> > > > > >
> > > > >
> > > > > I agree but it may be better to make it a PGC_SIGHUP parameter.
> > > > >
> > > > > > 4) Another option is to have Logical Replication Launcher(or a new
> > > > > > process) to launch slot-sync worker. But going by the current design
> > > > > > where we have only 1 slotsync worker, it may be an overhead to have an
> > > > > > additional manager process maintained.
> > > > > >
> > > > >
> > > > > I don't see any good reason to have an additional launcher process here.
> > > > >
> > > > > >
> > > > > > Thus weighing pros and cons of all these options, we have currently
> > > > > > implemented the bgworker approach (approach 3).  Any feedback is
> > > > > > welcome.
> > > > > >
> > > > >
> > > > > I vote to go for (2) unless we face difficulties in doing so but (3)
> > > > > is also okay especially if others also think so.
> > > >
> > > > I am not against any of the approaches but I still feel that when we
> > > > have a standard way of doing things (bgworker) we should not keep
> > > > adding code to do things in a special way unless there is a strong
> > > > reason to do so. Now we need to decide if 'enable_syncslot' being
> > > > PGC_POSTMASTER is a strong reason to go the non-standard way?
> > > >
> > >
> > > Agreed and as said earlier I think it is better to make it a
> > > PGC_SIGHUP. Also, not sure we can say it is a non-standard way as
> > > already autovacuum launcher is handled in the same way. One more minor
> > > thing is it will save us for having a new bgworker state
> > > BgWorkerStart_ConsistentState_HotStandby as introduced by this patch.
> >
> > Why do we need to add a new BgWorkerStart_ConsistentState_HotStandby
> > for the slotsync worker? Isn't it sufficient that the slotsync worker
> > exits if not in hot standby mode?
>
> It is doable, but that will mean starting slot-sync worker even on
> primary on every server restart which does not seem like a good idea.
> We wanted to have a way where-in it does not start itself in
> non-standby mode.

Understood.

Another idea would be that the startup process dynamically registers
the slotsync worker if hot_standby is enabled. But it doesn't seem
like the right approach.

>
> > Is there any technical difficulty or obstacle to make the slotsync
> > worker start using bgworker after reloading the config file?
>
> When we register slotsync worker as bgworker, we can only register the
> bgworker before initializing shared memory, we cannot register
> dynamically in the cycle of ServerLoop and thus we do not have
> flexibility of registering/deregistering the bgworker  (or controlling
> the bgworker start) based on config parameters each time they change.
> We can always start slot-sync worker and let it check if
> enable_syncslot is ON. If not, exit and retry the next time when
> postmaster will restart it after restart_time(60sec). The downside of
> this approach is, even if any user does not want slot-sync
> functionality and thus has permanently disabled 'enable_syncslot', it
> will keep on restarting and exiting there.

Thanks for the explanation. It sounds like it's not impossible but
would require some work. If allowing bgworkers to start also on SIGUP
is a general improvement, we can implement it later while having
enable_syncslot PGC_POSTMASTER at this time. Then, we will be able to
make the enable_syncslot PGC_SIGUP later.

BTW I think I found a race condition in the v61 patch to cause that
the slotsync worker continues working even after promotion (I've not
tested with v62 patch though). At the time when the startup shutdown
the slotsync worker in FinishWalRecovery(), the postmaster's pmState
is still PM_HOT_STANDBY. And if the slotsync worker is not running
when the startup process attempts to shutdown it, ShutDownSlotSync()
does nothing. Therefore, after the startup process doesn't shutdown
the slotsync worker, the postmaster could relaunch the slotsync worker
before its state transition to PM_RUN.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 04:33  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  3 siblings, 0 replies; 119+ messages in thread

From: Peter Smith @ 2024-01-17 04:33 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Here is a  review comment for the latest v62-0002 changes.

======
src/backend/replication/logical/slotsync.c

1.
+ if (namestrcmp(&slot->data.plugin, remote_slot->plugin) == 0 &&
+ slot->data.database == dbid &&
+ remote_slot->restart_lsn == slot->data.restart_lsn &&
+ remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+ remote_slot->two_phase == slot->data.two_phase &&
+ remote_slot->failover == slot->data.failover &&
+ remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+ return false;

For consistency, I think it would be better to always code the remote
slot value on the LHS and the local slot value on the RHS, instead of
the current random mix.

And rename 'dbid' to 'remote_dbid' for name consistency too.

SUGGESTION
if (namestrcmp(remote_slot->plugin, &slot->data.plugin) == 0 &&
  remote_dbid == slot->data.database &&
  remote_slot->restart_lsn == slot->data.restart_lsn &&
  remote_slot->catalog_xmin == slot->data.catalog_xmin &&
  remote_slot->two_phase == slot->data.two_phase &&
  remote_slot->failover == slot->data.failover &&
  remote_slot->confirmed_lsn == slot->data.confirmed_flush)
  return false;

======
Kind Regards,
Peter Smith.
Fujitsu Australia






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 04:37  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  3 siblings, 0 replies; 119+ messages in thread

From: Peter Smith @ 2024-01-17 04:37 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Tue, Jan 16, 2024 at 10:57 PM shveta malik <[email protected]> wrote:
>
...
> v62-006:
> Separated the failover-ready validation steps into this separate
> doc-patch (which were earlier present in v61-002 and v61-003). Also
> addressed some of the doc comments by Peter in [1].
> Thanks Hou-San for providing this patch.
>
> [1]: https://www.postgresql.org/message-id/CAHut%2BPteZVNx1jQ6Hs3mEdoC%3DDNALVpJJ2mZDYim7sU-04tiaw%40mail...
>

Thanks for addressing my previous review in the new patch 0006. I
checked it again and below are a few more comments

======
1. GENERAL

I was wondering if some other documentation (like somewhere from
chapter 27, or maybe the pgctl promote docs?) should be referring back
to this new information about how to decide if the standby is ready
for promotion.

======
doc/src/sgml/logical-replication.sgml

2.
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use <varname>standby_slot_names</varname> to
+   prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>

For easier navigation, perhaps that standby_slot_names should include
a link back to where the standby_slot_names GUC is described.

~~~

3.
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify the
+       slot names that should be synced to the standby that we plan to promote.

Minor change to wording.

SUGGESTION
Firstly, on the subscriber node, use the following SQL to identify
which slots should be synced to the standby that we plan to promote.

~~~

4.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid ||
'_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r,
pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub}
+(1 row)
+</programlisting></para>
+     </step>

I think the example might be better if the result shows > 1 slot.  e.g.
{sub1,sub2,sub3}

This would also make the next step 1.b. more clear.

~~~

5.
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server. This step can be skipped if
+       <varname>standby_slot_names</varname> has been correctly configured.
+<programlisting>
+test_standby=# SELECT bool_and(synced AND NOT temporary AND
conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name in ('sub');
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>

5a.

(uppercase SQL keyword)

/in/IN/

~

5b.
I felt this might be easier to understand if the SQL gives a
two-column result instead of one all-of-nothing T/F where you might no
be sure which slot was the one giving a problem. e.g.

failover_ready | slot
---------------------
t              | sub1
t              | sub2
f              | sub3
...

~~~

6.
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL. If the
+        query result is NULL, it indicates that the subscriber has not yet
+        replayed any WAL. Therefore, the next step can be skipped, as the
+        standby server must be ahead of the subscriber.

IMO all of that part "If the query result is NULL" does not really
belong here because it describes skipping the *next* step. So, it
would be better to say this in the next step. Something like:

SUGGESTION (for step 2b)
Next, on the standby server check that the last-received WAL location
is ahead of the replayed WAL location on the subscriber identified
above. If the above SQL result was NULL, it means the subscriber has
not yet replayed any WAL, so the standby server must be ahead of the
subscriber, and this step can be skipped.

~~~

7.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN
pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' ||
r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN
r.srsublsn END) as remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid =
r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' ||
s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>

7a.
(uppercase SQL keyword)

/as/AS/

~

7b.
missing table alias

/WHERE subfailover/WHERE s.subfailover/

~~~

8.
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.


See the review comment above (#6) which suggested adding some more info here.

======
Kind Regards,
Peter Smith.
Fujitsu Australia






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 04:48  shveta malik <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-17 04:48 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Wed, Jan 17, 2024 at 6:43 AM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Jan 16, 2024 at 6:40 PM shveta malik <[email protected]> wrote:
> >
> > On Tue, Jan 16, 2024 at 12:59 PM Masahiko Sawada <[email protected]> wrote:
> > >
> > > On Tue, Jan 16, 2024 at 1:07 PM Amit Kapila <[email protected]> wrote:
> > > >
> > > > On Tue, Jan 16, 2024 at 9:03 AM shveta malik <[email protected]> wrote:
> > > > >
> > > > > On Sat, Jan 13, 2024 at 12:54 PM Amit Kapila <[email protected]> wrote:
> > > > > >
> > > > > > On Fri, Jan 12, 2024 at 5:50 PM shveta malik <[email protected]> wrote:
> > > > > > >
> > > > > > > There are multiple approaches discussed and tried when it comes to
> > > > > > > starting a slot-sync worker. I am summarizing all here:
> > > > > > >
> > > > > > >  1) Make slotsync worker as an Auxiliary Process (like checkpointer,
> > > > > > > walwriter, walreceiver etc). The benefit this approach provides is, it
> > > > > > > can control begin and stop in a more flexible way as each auxiliary
> > > > > > > process could have different checks before starting and can have
> > > > > > > different stop conditions. But it needs code duplication for process
> > > > > > > management(start, stop, crash handling, signals etc) and currently it
> > > > > > > does not support db-connection smoothly (none of the auxiliary process
> > > > > > > has one so far)
> > > > > > >
> > > > > >
> > > > > > As slotsync worker needs to perform transactions and access syscache,
> > > > > > we can't make it an auxiliary process as that doesn't initialize the
> > > > > > required stuff like syscache. Also, see the comment "Auxiliary
> > > > > > processes don't run transactions ..." in AuxiliaryProcessMain() which
> > > > > > means this is not an option.
> > > > > >
> > > > > > >
> > > > > > > 2) Make slotsync worker as a 'special' process like AutoVacLauncher
> > > > > > > which is neither an Auxiliary process nor a bgworker one. It allows
> > > > > > > db-connection and also provides flexibility to have start and stop
> > > > > > > conditions for a process.
> > > > > > >
> > > > > >
> > > > > > Yeah, due to these reasons, I think this option is worth considering
> > > > > > and another plus point is that this allows us to make enable_syncslot
> > > > > > a PGC_SIGHUP GUC rather than a PGC_POSTMASTER.
> > > > > >
> > > > > > >
> > > > > > > 3) Make slotysnc worker a bgworker. Here we just need to register our
> > > > > > > process as a bgworker (RegisterBackgroundWorker()) by providing a
> > > > > > > relevant start_time and restart_time and then the process management
> > > > > > > is well taken care of. It does not need any code-duplication and
> > > > > > > allows db-connection smoothly in registered process. The only thing it
> > > > > > > lacks is that it does not provide flexibility of having
> > > > > > > start-condition which then makes us to have 'enable_syncslot' as
> > > > > > > PGC_POSTMASTER parameter rather than PGC_SIGHUP. Having said this, I
> > > > > > > feel enable_syncslot is something which will not be changed frequently
> > > > > > > and with the benefits provided by bgworker infra, it seems a
> > > > > > > reasonably good option to choose this approach.
> > > > > > >
> > > > > >
> > > > > > I agree but it may be better to make it a PGC_SIGHUP parameter.
> > > > > >
> > > > > > > 4) Another option is to have Logical Replication Launcher(or a new
> > > > > > > process) to launch slot-sync worker. But going by the current design
> > > > > > > where we have only 1 slotsync worker, it may be an overhead to have an
> > > > > > > additional manager process maintained.
> > > > > > >
> > > > > >
> > > > > > I don't see any good reason to have an additional launcher process here.
> > > > > >
> > > > > > >
> > > > > > > Thus weighing pros and cons of all these options, we have currently
> > > > > > > implemented the bgworker approach (approach 3).  Any feedback is
> > > > > > > welcome.
> > > > > > >
> > > > > >
> > > > > > I vote to go for (2) unless we face difficulties in doing so but (3)
> > > > > > is also okay especially if others also think so.
> > > > >
> > > > > I am not against any of the approaches but I still feel that when we
> > > > > have a standard way of doing things (bgworker) we should not keep
> > > > > adding code to do things in a special way unless there is a strong
> > > > > reason to do so. Now we need to decide if 'enable_syncslot' being
> > > > > PGC_POSTMASTER is a strong reason to go the non-standard way?
> > > > >
> > > >
> > > > Agreed and as said earlier I think it is better to make it a
> > > > PGC_SIGHUP. Also, not sure we can say it is a non-standard way as
> > > > already autovacuum launcher is handled in the same way. One more minor
> > > > thing is it will save us for having a new bgworker state
> > > > BgWorkerStart_ConsistentState_HotStandby as introduced by this patch.
> > >
> > > Why do we need to add a new BgWorkerStart_ConsistentState_HotStandby
> > > for the slotsync worker? Isn't it sufficient that the slotsync worker
> > > exits if not in hot standby mode?
> >
> > It is doable, but that will mean starting slot-sync worker even on
> > primary on every server restart which does not seem like a good idea.
> > We wanted to have a way where-in it does not start itself in
> > non-standby mode.
>
> Understood.
>
> Another idea would be that the startup process dynamically registers
> the slotsync worker if hot_standby is enabled. But it doesn't seem
> like the right approach.
>
> >
> > > Is there any technical difficulty or obstacle to make the slotsync
> > > worker start using bgworker after reloading the config file?
> >
> > When we register slotsync worker as bgworker, we can only register the
> > bgworker before initializing shared memory, we cannot register
> > dynamically in the cycle of ServerLoop and thus we do not have
> > flexibility of registering/deregistering the bgworker  (or controlling
> > the bgworker start) based on config parameters each time they change.
> > We can always start slot-sync worker and let it check if
> > enable_syncslot is ON. If not, exit and retry the next time when
> > postmaster will restart it after restart_time(60sec). The downside of
> > this approach is, even if any user does not want slot-sync
> > functionality and thus has permanently disabled 'enable_syncslot', it
> > will keep on restarting and exiting there.
>
> Thanks for the explanation. It sounds like it's not impossible but
> would require some work. If allowing bgworkers to start also on SIGUP
> is a general improvement, we can implement it later while having
> enable_syncslot PGC_POSTMASTER at this time. Then, we will be able to
> make the enable_syncslot PGC_SIGUP later.
>
> BTW I think I found a race condition in the v61 patch to cause that
> the slotsync worker continues working even after promotion (I've not
> tested with v62 patch though). At the time when the startup shutdown
> the slotsync worker in FinishWalRecovery(), the postmaster's pmState
> is still PM_HOT_STANDBY. And if the slotsync worker is not running
> when the startup process attempts to shutdown it, ShutDownSlotSync()
> does nothing. Therefore, after the startup process doesn't shutdown
> the slotsync worker, the postmaster could relaunch the slotsync worker
> before its state transition to PM_RUN.

Yes, this race condition exists. We have attempted to fix this
race-condition in v62-003 by introducing 'stopSignaled' bool in
slot-sync worker shared memory. The startup process will set it to
true before shutting slotsync worker and if meanwhile postmaster ends
up restarting it, ReplSlotSyncWorkerMain() will exit seeing
'stopSignaled' set. This is on similar line of WalReceiver where
postmaster starts it and startup process shuts it down and it handles
similar race condition by having state-machinery in place (see
WALRCV_STOPPING, WALRCV_STOPPED in ShutdownWalRcv() and
WalReceiverMain()).

Having said above, I feel in slotysnc worker case:
--I need to pull that race-condition fix around 'stopSignaled' to
patch002 instead.
--Also I feel in ShutDownSlotSync(), I need to move setting
'stopSignaled' to true before we exit on finding 'SlotSyncWorker->pid'
is InvalidPid. This will take care of that scenario too, where there
was no slot-sync worker present and startup process tried to shut it
down. We need this flag set in that corner-case too.

thanks
Shveta






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 05:11  Nisha Moond <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Nisha Moond @ 2024-01-17 05:11 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: shveta malik <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Bertrand Drouvot <[email protected]>; Peter Smith <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

A review on v62-006: failover-ready validation steps doc -

+       Next, check that the logical replication slots identified above exist on
+       the standby server. This step can be skipped if
+       <varname>standby_slot_names</varname> has been correctly configured.
+<programlisting>
+test_standby=# SELECT bool_and(synced AND NOT temporary AND
conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name in ('sub');
+ failover_ready
+----------------
+ t
+(1 row)

This query does not ensure that all the logical replication slots
exist on standby. Due to the 'IN ('slots')' check, it will return
'true' even if only one or a few slots exist.






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 09:38  Bertrand Drouvot <[email protected]>
  parent: shveta malik <[email protected]>
  3 siblings, 2 replies; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-17 09:38 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Hi,

On Tue, Jan 16, 2024 at 05:27:05PM +0530, shveta malik wrote:
> PFA v62. Details:

Thanks! 

> v62-003:
> It is a new patch which attempts to implement slot-sync worker as a
> special process which is neither a bgworker nor an Auxiliary process.
> Here we get the benefit of converting enable_syncslot to a PGC_SIGHUP
> Guc rather than PGC_POSTMASTER. We launch the slot-sync worker only if
> it is hot-standby and 'enable_syncslot' is ON.

The implementation looks reasonable to me (from what I can see some parts is
copy/paste from an already existing "special" process and some parts are
"sync slot" specific) which makes fully sense.

A few remarks:

1 ===
+                * Was it the slot sycn worker?

Typo: sycn

2 ===
+                * ones), and no walwriter, autovac launcher or bgwriter or slot sync

Instead? "* ones), and no walwriter, autovac launcher, bgwriter or slot sync"

3 ===
+ * restarting slot slyc worker. If stopSignaled is set, the worker will

Typo: slyc

4 ===
+/* Flag to tell if we are in an slot sync worker process */

s/an/a/ ?

5 === (coming from v62-0002)
+       Assert(tuplestore_tuple_count(res->tuplestore) == 1);

Is it even possible for the related query to not return only one row? (I think the 
"count" ensures it).

6 ===
        if (conninfo_changed ||
                primary_slotname_changed ||
+               old_enable_syncslot != enable_syncslot ||
                (old_hot_standby_feedback != hot_standby_feedback))
        {
                ereport(LOG,
                                errmsg("slot sync worker will restart because of"
                                           " a parameter change"));

I don't think "slot sync worker will restart" is true if one change enable_syncslot
from on to off.

IMHO, v62-003 is in good shape and could be merged in v62-002 (that would ease
the review). But let's wait to see if others think differently.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 10:30  shveta malik <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  1 sibling, 6 replies; 119+ messages in thread

From: shveta malik @ 2024-01-17 10:30 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Wed, Jan 17, 2024 at 3:08 PM Bertrand Drouvot
<[email protected]> wrote:
>
> Hi,
>
> On Tue, Jan 16, 2024 at 05:27:05PM +0530, shveta malik wrote:
> > PFA v62. Details:
>
> Thanks!
>
> > v62-003:
> > It is a new patch which attempts to implement slot-sync worker as a
> > special process which is neither a bgworker nor an Auxiliary process.
> > Here we get the benefit of converting enable_syncslot to a PGC_SIGHUP
> > Guc rather than PGC_POSTMASTER. We launch the slot-sync worker only if
> > it is hot-standby and 'enable_syncslot' is ON.
>
> The implementation looks reasonable to me (from what I can see some parts is
> copy/paste from an already existing "special" process and some parts are
> "sync slot" specific) which makes fully sense.
>
> A few remarks:
>
> 1 ===
> +                * Was it the slot sycn worker?
>
> Typo: sycn
>
> 2 ===
> +                * ones), and no walwriter, autovac launcher or bgwriter or slot sync
>
> Instead? "* ones), and no walwriter, autovac launcher, bgwriter or slot sync"
>
> 3 ===
> + * restarting slot slyc worker. If stopSignaled is set, the worker will
>
> Typo: slyc
>
> 4 ===
> +/* Flag to tell if we are in an slot sync worker process */
>
> s/an/a/ ?
>
> 5 === (coming from v62-0002)
> +       Assert(tuplestore_tuple_count(res->tuplestore) == 1);
>
> Is it even possible for the related query to not return only one row? (I think the
> "count" ensures it).
>
> 6 ===
>         if (conninfo_changed ||
>                 primary_slotname_changed ||
> +               old_enable_syncslot != enable_syncslot ||
>                 (old_hot_standby_feedback != hot_standby_feedback))
>         {
>                 ereport(LOG,
>                                 errmsg("slot sync worker will restart because of"
>                                            " a parameter change"));
>
> I don't think "slot sync worker will restart" is true if one change enable_syncslot
> from on to off.
>
> IMHO, v62-003 is in good shape and could be merged in v62-002 (that would ease
> the review). But let's wait to see if others think differently.
>
> Regards,
>
> --
> Bertrand Drouvot
> PostgreSQL Contributors Team
> RDS Open Source Databases
> Amazon Web Services: https://aws.amazon.com


PFA v63.

--It addresses comments by Peter given in [1], [2], comment by Nisha
given in [3], comments by Bertrand given in [4]
--It also moves race-condition fix from patch003 to patch002 as
suggested by Swada-san offlist. Race-condition is mentioned in [5]

All the changes are in patch02, patch003 and patch006.

[1]: https://www.postgresql.org/message-id/CAHut%2BPuECB8fNBfXMdTHSMKF9kL%3D0XqPw1Am4NVahfJSSHzoYg%40mail...
[2]: https://www.postgresql.org/message-id/CAHut%2BPt0uum%2B6Hg5UDofWMEJWhVEyArM1b0_B94UJmRcQmz7DA%40mail...
[3]: https://www.postgresql.org/message-id/CABdArM73qdHyA0nteDLAQrfKNHRP%2B5Qq6p8uobg5bkE3EWiC%2Bg%40mail...
[4]: https://www.postgresql.org/message-id/ZaegJe9JpUiQeV%2BD%40ip-10-97-1-34.eu-west-3.compute.internal
[5]: https://www.postgresql.org/message-id/CAD21AoA5izeKpp9Ei4Cd745pKX3wn-TRvhhmPFEW9UY1nx%2B_aw%40mail.g...

thanks
Shveta


Attachments:

  [application/octet-stream] v63-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.3K, ../../CAJpy0uBc6H22RbNVU213AZ_BPw+uDptcTVHakKegpTCv74yN=w@mail.gmail.com/2-v63-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 2000a613313e77f5d00f6a482bd25b807403a331 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 16:08:07 +0530
Subject: [PATCH v63 4/6] Allow logical walsenders to wait for the physical
 standbys

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   4 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 733 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index 6721d8951d..edb511c065 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -355,7 +355,9 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      be enabled on the standby. It's also highly recommended that the said
      physical replication slot is named in <varname>standby_slot_names</varname>
      list on the primary, to prevent the subscriber from consuming changes
-     faster than the hot standby.
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in <varname>standby_slot_names</varname>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 33f957b02f..d0509fb5a5 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2210,3 +2224,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index c81a7a8344..acf562fe93 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3527,6 +3599,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3596,8 +3669,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0879bab57e..fead31748e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5763454336..08db4c37bc 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4618,6 +4618,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 6830f7d16b..0c7b6117e0 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f81bef9e42..eef9f25c45 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..9a42b01f9a 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -45,6 +45,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index c17abfb40b..d50bbb3ae7 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v63-0003-Slot-sync-worker-as-a-special-process.patch (35.8K, ../../CAJpy0uBc6H22RbNVU213AZ_BPw+uDptcTVHakKegpTCv74yN=w@mail.gmail.com/3-v63-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From 0a2dcdb562cd1fc8f3bde5b972699b0dbde349d1 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Wed, 17 Jan 2024 13:56:25 +0530
Subject: [PATCH v63 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to 'enable_syncslot' as PGC_SIGHUP which was otherwise
a PGC_POSTMASTER GUC when slotsync worker was registered as bgworker.
---
 doc/src/sgml/bgworker.sgml                 |  65 +----
 src/backend/postmaster/bgworker.c          |   3 -
 src/backend/postmaster/postmaster.c        |  85 ++++--
 src/backend/replication/logical/slotsync.c | 323 ++++++++++++++++-----
 src/backend/storage/lmgr/proc.c            |   9 +-
 src/backend/tcop/postgres.c                |  11 -
 src/backend/utils/activity/pgstat_io.c     |   1 +
 src/backend/utils/init/miscinit.c          |   7 +-
 src/backend/utils/init/postinit.c          |   8 +-
 src/backend/utils/misc/guc_tables.c        |   2 +-
 src/include/miscadmin.h                    |   1 +
 src/include/postmaster/bgworker.h          |   1 -
 src/include/replication/worker_internal.h  |   7 +-
 13 files changed, 350 insertions(+), 173 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..471ee5eccc 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,9 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY)
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1015,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1835,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2686,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3045,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3217,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit (FATAL can
+		 * be caused by libpqwalreceiver on receiving shutdown request by the
+		 * startup process during promotion) can be ignored; we'll start a new
+		 * one at the next iteration of the postmaster's main loop, if
+		 * necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("Slotsync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3599,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3745,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3762,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3778,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3877,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4101,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4915,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5019,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5848,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index f90ebef9d0..c54ae88880 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -27,6 +27,8 @@
  * It waits for a period of time before the next synchronization, with the
  * duration varying based on whether any slots were updated during the last
  * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
  *---------------------------------------------------------------------------
  */
 
@@ -40,7 +42,9 @@
 #include "commands/dbcommands.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -53,6 +57,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -97,13 +103,24 @@ SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
 /* GUC variable */
 bool		enable_syncslot = false;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 /*
  * Sleep time in ms between slot-sync cycles.
  * See wait_for_slot_activity() for how we adjust this
  */
 static long sleep_ms;
 
+/* Min and Max sleep time for slot sync worker */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
 
 /*
  * If necessary, update local slot metadata based on the data from the remote
@@ -688,7 +705,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -703,8 +721,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -742,12 +762,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("skipping slot synchronization due to bad configuration"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -757,18 +780,18 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 
 /*
  * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * appropriately. If not, log the message and pass 'valid' as false
+ * to the caller.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
  * returns it.
  */
 static char *
-validate_parameters_and_get_dbname(void)
+validate_parameters_and_get_dbname(bool *valid)
 {
 	char	   *dbname;
 
-	/* Sanity check. */
-	Assert(enable_syncslot);
+	*valid = false;
 
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
@@ -777,10 +800,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_slot_name"));
+		return NULL;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -788,30 +814,39 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be enabled.", "hot_standby_feedback"));
+		return NULL;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("wal_level must be >= logical."));
+		return NULL;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_conninfo"));
+		return NULL;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
@@ -819,14 +854,61 @@ validate_parameters_and_get_dbname(void)
 	 */
 	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
 	if (dbname == NULL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+		return NULL;
+	}
+
+	/* All good, set valid to true now */
+	*valid = true;
+
+	return dbname;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+	bool		valid;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		dbname = validate_parameters_and_get_dbname(&valid);
+		if (valid)
+			break;
+		else
+		{
+			ProcessSlotSyncInterrupts(NULL);
+
+			rc = WaitLatch(MyLatch,
+						   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+						   MAX_WORKER_NAPTIME_MS,
+						   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+			if (rc & WL_LATCH_SET)
+				ResetLatch(MyLatch);
+
+		}
+	}
 
 	return dbname;
 }
@@ -842,6 +924,7 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
@@ -852,6 +935,22 @@ slotsync_reread_config(void)
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		/*
+		 * We have reached here, so old value must be true and new must be
+		 * false.
+		 */
+		Assert(old_enable_syncslot);
+		Assert(!enable_syncslot);
+
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -859,8 +958,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of"
 					   " a parameter change"));
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -877,7 +975,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down"
 					   " on receiving SIGINT"));
@@ -910,20 +1009,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
-#define MIN_WORKER_NAPTIME_MS  200
-#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
-
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -959,20 +1054,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
+
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
 
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -987,26 +1100,69 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	BackgroundWorkerUnblockSignals();
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1025,26 +1181,27 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1061,7 +1218,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1135,42 +1292,64 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("enable_syncslot is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
+
+	Assert(ac < lengthof(av));
+
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	memset(&bgw, 0, sizeof(bgw));
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+#else
+	switch ((pid = fork_process()))
+#endif
+	{
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+#ifndef EXEC_BACKEND
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..ad1352bf76 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,11 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker does not participate in it, see comments atop Backend.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -935,7 +939,8 @@ ProcKill(int code, Datum arg)
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 19b08c1b5f..1eaaf3c6c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..c5de528b34 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -341,6 +341,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
+		case B_SLOTSYNC_WORKER:
 			return true;
 	}
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..d5e16f1df4 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -311,6 +312,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_WAL_WRITER:
 			backendDesc = "walwriter";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 	}
 
 	return backendDesc;
@@ -837,7 +841,8 @@ InitializeSessionUserIdStandalone(void)
 	 * This function should only be called in single-user mode, in autovacuum
 	 * workers, and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0f5ec63de1..5763454336 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2046,7 +2046,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..e89b8121f3 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -337,6 +337,7 @@ typedef enum BackendType
 	B_WAL_RECEIVER,
 	B_WAL_SENDER,
 	B_WAL_SUMMARIZER,
+	B_SLOTSYNC_WORKER,
 	B_WAL_WRITER,
 } BackendType;
 
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..cd530d3d40 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,10 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v63-0001-Enable-setting-failover-property-for-a-slot-thro.patch (100.3K, ../../CAJpy0uBc6H22RbNVU213AZ_BPw+uDptcTVHakKegpTCv74yN=w@mail.gmail.com/4-v63-0001-Enable-setting-failover-property-for-a-slot-thro.patch)
  download | inline diff:
From 7261144861853ad84df0d611b26ccabec1758435 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 4 Jan 2024 09:15:26 +0530
Subject: [PATCH v63 1/6] Enable setting failover property for a slot through
 SQL API and subscription commands

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the next patch will
address that.

In addition, a new replication command named ALTER_REPLICATION_SLOT and a
corresponding walreceiver API function named walrcv_alter_slot have been
implemented. These additions provide subscribers or users the ability to
modify the failover property of a replication slot on the publisher.

Moreover, a new subscription option called 'failover' has been added,
allowing users to set it when creating or altering a subscription. Also,
a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out       |  58 ++++++
 contrib/test_decoding/sql/slot.sql            |  13 ++
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/func.sgml                        |  11 +-
 doc/src/sgml/protocol.sgml                    |  52 ++++++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 doc/src/sgml/system-views.sgml                |  11 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_functions.sql      |   1 +
 src/backend/catalog/system_views.sql          |   6 +-
 src/backend/commands/subscriptioncmds.c       | 105 ++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  38 +++-
 src/backend/replication/logical/tablesync.c   |   1 +
 src/backend/replication/logical/worker.c      |   7 +
 src/backend/replication/repl_gram.y           |  20 ++-
 src/backend/replication/repl_scanner.l        |   2 +
 src/backend/replication/slot.c                |  33 +++-
 src/backend/replication/slotfuncs.c           |  22 ++-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |  64 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/info.c                     |   5 +-
 src/bin/pg_upgrade/pg_upgrade.c               |   6 +-
 src/bin/pg_upgrade/pg_upgrade.h               |   2 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_proc.dat               |  14 +-
 src/include/catalog/pg_subscription.h         |  11 ++
 src/include/nodes/replnodes.h                 |  12 ++
 src/include/replication/slot.h                |   9 +-
 src/include/replication/walreceiver.h         |  18 +-
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 39 files changed, 745 insertions(+), 124 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..f224327c00 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the physical standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..154c426df7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        physical standbys so that logical replication can be resumed
+        after failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..af997efd2b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..ce386a46a7 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the physical standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..1868b95836 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the physical
+       standbys so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
@@ -1357,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..f50be29d99 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,30 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set failover for a subscription that does not have a slot name")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified if
+					 * the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1512,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error
+	 * while doing the database operations we won't be able to rollback
+	 * altered slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 201c36cb22..c5232cee2f 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -74,8 +74,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -96,6 +99,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -899,8 +903,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -929,7 +933,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -998,6 +1003,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 911835c5cb..3dea10f9b3 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..696376400e 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to physical standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -311,6 +314,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -679,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, true);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..c93dba855b 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -133,7 +135,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +174,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +192,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +431,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -782,11 +789,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e00395ff2b..ffacd55e5c 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,13 +1217,14 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 6bfdb5f008..c4aebd13b1 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 58811a6530..b9e747d72f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11115,17 +11115,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ca32625585..c92a81e6b7 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,12 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -145,6 +151,11 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..585ccbb504 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for physical standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,9 +224,10 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 29fd1cae64..7782c12d2e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3874,6 +3875,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v63-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uBc6H22RbNVU213AZ_BPw+uDptcTVHakKegpTCv74yN=w@mail.gmail.com/5-v63-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From e455ae84a62b7a7d78ef1dd4a276c874b65190e6 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 16:22:05 +0530
Subject: [PATCH v63 5/6] Non replication connection and app_name change.

Changes in this patch:
1) Convert replication connection to non-replication one in slotsync worker.
2) Use app_name as {cluster_name}_slotsyncworker in the slotsync worker
connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index f50be29d99..13da6b1466 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1530,7 +1532,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1781,7 +1784,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 934c1a3ab0..d6f0cae0a9 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -50,7 +53,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -125,7 +129,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -136,8 +145,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	const char *keys[6];
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index c54ae88880..d711e8f9ba 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1063,6 +1063,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1164,13 +1165,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 3dea10f9b3..95e7324c8f 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index ffacd55e5c..cfe647ec58 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 5e942cb4fc..68ac074274 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -434,8 +435,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v63-0002-Add-logical-slot-sync-capability-to-the-physical.patch (83.3K, ../../CAJpy0uBc6H22RbNVU213AZ_BPw+uDptcTVHakKegpTCv74yN=w@mail.gmail.com/6-v63-0002-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 0fca5f53107b7afef839319f0b50faa7b607d44f Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Fri, 12 Jan 2024 20:41:19 +0800
Subject: [PATCH v63 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   33 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1176 +++++++++++++++++
 src/backend/replication/slot.c                |   32 +-
 src/backend/replication/slotfuncs.c           |   14 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |    4 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    2 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   19 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 33 files changed, 1694 insertions(+), 37 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..6721d8951d 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -346,6 +346,39 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      <function>pg_log_standby_snapshot</function> function on the primary.
     </para>
 
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the failover option during slot creation and setting
+     <xref linkend="guc-enable-syncslot"/> on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and <varname>hot_standby_feedback</varname> must
+     be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in <varname>standby_slot_names</varname>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
+
     <caution>
      <para>
       Replication slots persist across crashes and know nothing about the state
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1868b95836..4f36a2f732 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index c5232cee2f..934c1a3ab0 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -34,6 +34,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -100,6 +102,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -432,6 +435,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..f90ebef9d0
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1176 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * Sleep time in ms between slot-sync cycles.
+ * See wait_for_slot_activity() for how we adjust this
+ */
+static long sleep_ms;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			remote_dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	remote_dbid = get_database_oid(remote_slot->database, false);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	SpinLockAcquire(&slot->mutex);
+	namestrcpy(&slot->data.plugin, remote_slot->plugin);
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true either if the slot is marked as RS_PERSISTENT (sync-ready) or
+ * is synced periodically (if it was already sync-ready). Return false
+ * otherwise.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	(void) local_slot_update(remote_slot);
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly locally created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	if (remote_slot->confirmed_lsn > latestWalEnd)
+	{
+		elog(ERROR, "exiting from slot synchronization as the received slot sync"
+			 " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+			 LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+			 remote_slot->name,
+			 LSN_FORMAT_ARGS(latestWalEnd));
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization on receiving"
+						   " the failover slot \"%s\" from the primary server",
+						   remote_slot->name),
+					errdetail("A user-created slot with the same name already"
+							  " exists on the standby."));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(remote_slot->database, false);
+		namestrcpy(&slot->data.plugin, remote_slot->plugin);
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info"
+					   " from the primary server: %s", res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, 7, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 8, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 9, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the"
+					   " primary server: %s", PrimarySlotName, res->err));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	(void) tuplestore_gettupleslot(res->tuplestore, true, false, tupslot);
+
+	/* It must return one tuple */
+	Assert(tuplestore_tuple_count(res->tuplestore) == 1);
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("wal_level must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of"
+					   " a parameter change"));
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down"
+					   " on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for logical replication launcher.
+ *
+ * Called on logical replication launcher exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the logical replication launcher, or when there
+	 * is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("enable_syncslot is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 696376400e..33f957b02f 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to physical standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -315,6 +316,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +682,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +711,16 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	/*
+	 * Do not allow users to alter the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot alter replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +733,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +889,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index c93dba855b..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..c81a7a8344 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index e5119ed55d..04fed1007e 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
@@ -342,6 +343,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1eaaf3c6c5..19b08c1b5f 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index f625473ad4..0879bab57e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,8 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_PRIMARY_CATCHUP	"Waiting for the primary to catch-up, in slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index e53ebc6dc2..0f5ec63de1 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2044,6 +2045,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 835b0e9ba8..6830f7d16b 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b9e747d72f..6dc234f9f7 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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 585ccbb504..f81bef9e42 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for physical standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..5e942cb4fc 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,21 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbinfo_for_failover_slots_fn
+ *
+ * Run LIST_DBID_FOR_FAILOVER_SLOTS on primary server to get the
+ * list of unique DBIDs for failover logical slots
+ */
+typedef List *(*walrcv_get_dbinfo_for_failover_slots_fn) (WalReceiverConn *conn);
+
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +418,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +444,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +503,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..c17abfb40b 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly locally created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT failover, synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t|t",
+	'logical slot has failover as true and synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 271313ebf8..aac83755de 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -132,8 +132,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(22 rows)
+(23 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7782c12d2e..10a73ba633 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2321,6 +2321,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2581,6 +2582,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v63-0006-Document-the-steps-to-check-if-the-standby-is-re.patch (9.4K, ../../CAJpy0uBc6H22RbNVU213AZ_BPw+uDptcTVHakKegpTCv74yN=w@mail.gmail.com/7-v63-0006-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From 36062b7ae0c0ae83918dd467764d863d48c91e33 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Tue, 16 Jan 2024 14:02:32 +0800
Subject: [PATCH v63 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 doc/src/sgml/logicaldecoding.sgml     |  33 ++++---
 3 files changed, 158 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 9dd52ff275..11f41aea2c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1479,6 +1479,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    In one has opted for synchronization of logical slots as mentioned in
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>,
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps mentioned in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index edb511c065..f2a0b5fa7b 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -346,9 +346,27 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      <function>pg_log_standby_snapshot</function> function on the primary.
     </para>
 
+    <caution>
+     <para>
+      Replication slots persist across crashes and know nothing about the state
+      of their consumer(s). They will prevent removal of required resources
+      even when there is no connection using them. This consumes storage
+      because neither required WAL nor required rows from the system catalogs
+      can be removed by <command>VACUUM</command> as long as they are required by a replication
+      slot.  In extreme cases this could cause the database to shut down to prevent
+      transaction ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
+      So if a slot is no longer required it should be dropped.
+     </para>
+    </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slots Synchronization</title>
     <para>
      A logical replication slot on the primary can be synchronized to the hot
-     standby by enabling the failover option during slot creation and setting
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
      <xref linkend="guc-enable-syncslot"/> on the standby. For the synchronization
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and <varname>hot_standby_feedback</varname> must
@@ -380,19 +398,6 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      It is recommended that subscriptions are first disabled before promoting
      the standby and are enabled back after altering the connection string.
     </para>
-
-    <caution>
-     <para>
-      Replication slots persist across crashes and know nothing about the state
-      of their consumer(s). They will prevent removal of required resources
-      even when there is no connection using them. This consumes storage
-      because neither required WAL nor required rows from the system catalogs
-      can be removed by <command>VACUUM</command> as long as they are required by a replication
-      slot.  In extreme cases this could cause the database to shut down to prevent
-      transaction ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
-      So if a slot is no longer required it should be dropped.
-     </para>
-    </caution>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-17 10:36  shveta malik <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-17 10:36 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Wed, Jan 17, 2024 at 3:08 PM Bertrand Drouvot
<[email protected]> wrote:
>
> Hi,
>
> On Tue, Jan 16, 2024 at 05:27:05PM +0530, shveta malik wrote:
> > PFA v62. Details:
>
> Thanks!
>
> > v62-003:
> > It is a new patch which attempts to implement slot-sync worker as a
> > special process which is neither a bgworker nor an Auxiliary process.
> > Here we get the benefit of converting enable_syncslot to a PGC_SIGHUP
> > Guc rather than PGC_POSTMASTER. We launch the slot-sync worker only if
> > it is hot-standby and 'enable_syncslot' is ON.
>
> The implementation looks reasonable to me (from what I can see some parts is
> copy/paste from an already existing "special" process and some parts are
> "sync slot" specific) which makes fully sense.

Thanks for the feedback. I have addressed the comments in v63 except 5th one.

> A few remarks:
>
> 1 ===
> +                * Was it the slot sycn worker?
>
> Typo: sycn
>
> 2 ===
> +                * ones), and no walwriter, autovac launcher or bgwriter or slot sync
>
> Instead? "* ones), and no walwriter, autovac launcher, bgwriter or slot sync"
>
> 3 ===
> + * restarting slot slyc worker. If stopSignaled is set, the worker will
>
> Typo: slyc
>
> 4 ===
> +/* Flag to tell if we are in an slot sync worker process */
>
> s/an/a/ ?
>
> 5 === (coming from v62-0002)
> +       Assert(tuplestore_tuple_count(res->tuplestore) == 1);
>
> Is it even possible for the related query to not return only one row? (I think the
> "count" ensures it).

I think you are right. This assertion was added sometime back on the
basis of feedback on hackers. Let me review that again. I can consider
this comment in the next version.

> 6 ===
>         if (conninfo_changed ||
>                 primary_slotname_changed ||
> +               old_enable_syncslot != enable_syncslot ||
>                 (old_hot_standby_feedback != hot_standby_feedback))
>         {
>                 ereport(LOG,
>                                 errmsg("slot sync worker will restart because of"
>                                            " a parameter change"));
>
> I don't think "slot sync worker will restart" is true if one change enable_syncslot
> from on to off.

Yes, right. I have changed the log-msg in this specific case.

>
> IMHO, v62-003 is in good shape and could be merged in v62-002 (that would ease
> the review). But let's wait to see if others think differently.
>
> Regards,
>
> --
> Bertrand Drouvot
> PostgreSQL Contributors Team
> RDS Open Source Databases
> Amazon Web Services: https://aws.amazon.com






^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-18 05:01  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  5 siblings, 1 reply; 119+ messages in thread

From: Peter Smith @ 2024-01-18 05:01 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

I have one question about the new code in v63-0002.

======
src/backend/replication/logical/slotsync.c

1. ReplSlotSyncWorkerMain

+ Assert(SlotSyncWorker->pid == InvalidPid);
+
+ /*
+ * Startup process signaled the slot sync worker to stop, so if meanwhile
+ * postmaster ended up starting the worker again, exit.
+ */
+ if (SlotSyncWorker->stopSignaled)
+ {
+ SpinLockRelease(&SlotSyncWorker->mutex);
+ proc_exit(0);
+ }

Can we be sure a worker crash can't occur (in ShutDownSlotSync?) in
such a way that SlotSyncWorker->stopSignaled was already assigned
true, but SlotSyncWorker->pid was not yet reset to InvalidPid;

e.g. Is the Assert above still OK?

======
Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-18 11:19  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  5 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-18 11:19 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
>
> PFA v63.
>

1.
+ /* User created slot with the same name exists, raise ERROR. */
+ if (!synced)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("exiting from slot synchronization on receiving"
+    " the failover slot \"%s\" from the primary server",
+    remote_slot->name),
+ errdetail("A user-created slot with the same name already"
+   " exists on the standby."));

I think here primary error message should contain the reason for
failure. Something like: "exiting from slot synchronization because
same name slot already exists on standby" then we can add more details
in errdetail.

2.
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
{
...
+ LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+ xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+ SpinLockAcquire(&slot->mutex);
+ slot->data.catalog_xmin = xmin_horizon;
+ SpinLockRelease(&slot->mutex);
...
}

Here, why slot->effective_catalog_xmin is not updated? The same is
required by a later call to ReplicationSlotsComputeRequiredXmin(). I
see that the prior version v60-0002 has the corresponding change but
it is missing in the latest version. Any reason?

3.
+ * Return true either if the slot is marked as RS_PERSISTENT (sync-ready) or
+ * is synced periodically (if it was already sync-ready). Return false
+ * otherwise.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)

The second part of the above comment (or is synced periodically (if it
was already sync-ready)) is not clear to me. Does it intend to
describe the case when we try to update the already created temp slot
in the last call. If so, that is not very clear because periodically
sounds like it can be due to repeated sync for sync-ready slot.

4.
+update_and_persist_slot(RemoteSlot *remote_slot)
{
...
+ (void) local_slot_update(remote_slot);
...
}

Can we write a comment to state the reason why we don't care about the
return value here?

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-18 12:55  Zhijie Hou (Fujitsu) <[email protected]>
  parent: shveta malik <[email protected]>
  5 siblings, 0 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-18 12:55 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wednesday, January 17, 2024 6:30 PM shveta malik <[email protected]> wrote:
> PFA v63.

I analyzed the security of the slotsync worker and replication connection a bit,
and didn't find issue. Here is detail: 

1) data security

First, we are using the role used in primary_conninfo, the role used here is
requested to have REPLICATION or SUPERUSER privilege[1] which means it is
reasonable for the role to modify and read replication slots on the primary.

On the primary, the slotsync worker only queries the pg_replication_view which
doesn't contain any system or user table access, so I think it's safe.

On the standby server, the slot sync worker will not read/write any user table as
well, thus we don't have the risk of executing arbitrary codes in trigger.

2) privilege check

The SQL query of the slotsync worker will take common privilege check on the
primary. If I revoke the function execution privilege on
pg_get_replication_slots from replication user, then the slotsync worker
won't be able to query the pg_replication_slots view. Same is true for the
pg_is_in_recovery function. The slotsync worker will keep reporting ERROR after
revoking which is as expected.

Based on above, I didn't see some security issues for slotsync worker.

[1] https://www.postgresql.org/docs/16/runtime-config-replication.html#GUC-PRIMARY-CONNINFO

Best Regards,
Hou zj


^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-18 23:11  Peter Smith <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Peter Smith @ 2024-01-18 23:11 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: shveta malik <[email protected]>; Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Drouvot, Bertrand <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Tue, Jan 9, 2024 at 11:15 PM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Tuesday, January 9, 2024 9:17 AM Peter Smith <[email protected]> wrote:
> >
...
> >
> > 2. ALTER_REPLICATION_SLOT ... FAILOVER
> >
> > +      <variablelist>
> > +       <varlistentry>
> > +        <term><literal>FAILOVER [ <replaceable
> > class="parameter">boolean</replaceable> ]</literal></term>
> > +        <listitem>
> > +         <para>
> > +          If true, the slot is enabled to be synced to the physical
> > +          standbys so that logical replication can be resumed after failover.
> > +         </para>
> > +        </listitem>
> > +       </varlistentry>
> > +      </variablelist>
> >
> > This syntax says passing the boolean value is optional. So it needs to be
> > specified here in the docs that not passing a value would be the same as
> > passing the value true.
>
> The behavior that "not passing a value would be the same as passing the value
> true " is due to the rule of defGetBoolean(). And all the options of commands
> in this document behave the same in this case, therefore I think we'd better
> add document for it in a general place in a separate patch/thread instead of
> mentioning this in each option's paragraph.
>

Hi Hou-san,

I did as suggested and posted a patch for this in another thread [1].
Please see if it is OK.

======
[1] https://www.postgresql.org/message-id/CAHut%2BPtDWSmW8uiRJF1LfGQJikmo7V2jdysLuRmtsanNZc7fNw%40mail.g...

Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 03:41  shveta malik <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-19 03:41 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Thu, Jan 18, 2024 at 10:31 AM Peter Smith <[email protected]> wrote:
>
> I have one question about the new code in v63-0002.
>
> ======
> src/backend/replication/logical/slotsync.c
>
> 1. ReplSlotSyncWorkerMain
>
> + Assert(SlotSyncWorker->pid == InvalidPid);
> +
> + /*
> + * Startup process signaled the slot sync worker to stop, so if meanwhile
> + * postmaster ended up starting the worker again, exit.
> + */
> + if (SlotSyncWorker->stopSignaled)
> + {
> + SpinLockRelease(&SlotSyncWorker->mutex);
> + proc_exit(0);
> + }
>
> Can we be sure a worker crash can't occur (in ShutDownSlotSync?) in
> such a way that SlotSyncWorker->stopSignaled was already assigned
> true, but SlotSyncWorker->pid was not yet reset to InvalidPid;
>
> e.g. Is the Assert above still OK?

We are good with the Assert here. I tried below cases:

1) When slotsync worker is say killed using 'kill', it is considered
as SIGTERM; slot sync worker invokes 'slotsync_worker_onexit()' before
going down and thus sets SlotSyncWorker->pid = InvalidPid. This means
when it is restarted (considering we have put the breakpoints in such
a way that postmaster had already reached do_start_bgworker() before
promotion finished), it is able to see stopSignaled set but pid is
InvalidPid and thus we are good.

2) Another case is when we kill slot sync worker using 'kill -9' (or
say we make it crash), in such a case, postmaster signals each sibling
process to quit (including startup process) and cleans up the shared
memory used by each (including SlotSyncWorker). In such a case
promotion fails. And if slot sync worker is started again, it will
find pid as InvalidPid. So we are good.

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 05:05  Masahiko Sawada <[email protected]>
  parent: shveta malik <[email protected]>
  5 siblings, 1 reply; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-19 05:05 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 17, 2024 at 7:30 PM shveta malik <[email protected]> wrote:
>
> On Wed, Jan 17, 2024 at 3:08 PM Bertrand Drouvot
> <[email protected]> wrote:
> >
> > Hi,
> >
> > On Tue, Jan 16, 2024 at 05:27:05PM +0530, shveta malik wrote:
> > > PFA v62. Details:
> >
> > Thanks!
> >
> > > v62-003:
> > > It is a new patch which attempts to implement slot-sync worker as a
> > > special process which is neither a bgworker nor an Auxiliary process.
> > > Here we get the benefit of converting enable_syncslot to a PGC_SIGHUP
> > > Guc rather than PGC_POSTMASTER. We launch the slot-sync worker only if
> > > it is hot-standby and 'enable_syncslot' is ON.
> >
> > The implementation looks reasonable to me (from what I can see some parts is
> > copy/paste from an already existing "special" process and some parts are
> > "sync slot" specific) which makes fully sense.
> >
> > A few remarks:
> >
> > 1 ===
> > +                * Was it the slot sycn worker?
> >
> > Typo: sycn
> >
> > 2 ===
> > +                * ones), and no walwriter, autovac launcher or bgwriter or slot sync
> >
> > Instead? "* ones), and no walwriter, autovac launcher, bgwriter or slot sync"
> >
> > 3 ===
> > + * restarting slot slyc worker. If stopSignaled is set, the worker will
> >
> > Typo: slyc
> >
> > 4 ===
> > +/* Flag to tell if we are in an slot sync worker process */
> >
> > s/an/a/ ?
> >
> > 5 === (coming from v62-0002)
> > +       Assert(tuplestore_tuple_count(res->tuplestore) == 1);
> >
> > Is it even possible for the related query to not return only one row? (I think the
> > "count" ensures it).
> >
> > 6 ===
> >         if (conninfo_changed ||
> >                 primary_slotname_changed ||
> > +               old_enable_syncslot != enable_syncslot ||
> >                 (old_hot_standby_feedback != hot_standby_feedback))
> >         {
> >                 ereport(LOG,
> >                                 errmsg("slot sync worker will restart because of"
> >                                            " a parameter change"));
> >
> > I don't think "slot sync worker will restart" is true if one change enable_syncslot
> > from on to off.
> >
> > IMHO, v62-003 is in good shape and could be merged in v62-002 (that would ease
> > the review). But let's wait to see if others think differently.
> >
> > Regards,
> >
> > --
> > Bertrand Drouvot
> > PostgreSQL Contributors Team
> > RDS Open Source Databases
> > Amazon Web Services: https://aws.amazon.com
>
>
> PFA v63.
>
> --It addresses comments by Peter given in [1], [2], comment by Nisha
> given in [3], comments by Bertrand given in [4]
> --It also moves race-condition fix from patch003 to patch002 as
> suggested by Swada-san offlist. Race-condition is mentioned in [5]
>

Thank you for updating the patch. I have some comments:

---
+        latestWalEnd = GetWalRcvLatestWalEnd();
+        if (remote_slot->confirmed_lsn > latestWalEnd)
+        {
+                elog(ERROR, "exiting from slot synchronization as the
received slot sync"
+                         " LSN %X/%X for slot \"%s\" is ahead of the
standby position %X/%X",
+                         LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+                         remote_slot->name,
+                         LSN_FORMAT_ARGS(latestWalEnd));
+        }

IIUC GetWalRcvLatestWalEnd () returns walrcv->latestWalEnd, which is
typically the primary server's flush position and doesn't mean the LSN
where the walreceiver received/flushed up to. Does it really happen
that the slot's confirmed_flush_lsn is higher than the primary's flush
lsn?

---
After dropping a database on the primary, I got the following LOG (PID
2978463 is the slotsync worker on the standby):

LOG:  still waiting for backend with PID 2978463 to accept ProcSignalBarrier
CONTEXT:  WAL redo at 0/301CE00 for Database/DROP: dir 1663/16384

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 05:52  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-19 05:52 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 17, 2024 at 4:06 PM shveta malik <[email protected]> wrote:
>
> >
> > 5 === (coming from v62-0002)
> > +       Assert(tuplestore_tuple_count(res->tuplestore) == 1);
> >
> > Is it even possible for the related query to not return only one row? (I think the
> > "count" ensures it).
>
> I think you are right. This assertion was added sometime back on the
> basis of feedback on hackers. Let me review that again. I can consider
> this comment in the next version.
>

OTOH, can't we keep the assert as it is but remove "= 1" from
"count(*) = 1" in the query. There shouldn't be more than one slot
with same name on the primary. Or, am I missing something?

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 06:16  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-19 06:16 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Fri, Jan 19, 2024 at 11:23 AM Amit Kapila <[email protected]> wrote:
>
> > > 5 === (coming from v62-0002)
> > > +       Assert(tuplestore_tuple_count(res->tuplestore) == 1);
> > >
> > > Is it even possible for the related query to not return only one row? (I think the
> > > "count" ensures it).
> >
> > I think you are right. This assertion was added sometime back on the
> > basis of feedback on hackers. Let me review that again. I can consider
> > this comment in the next version.
> >
>
> OTOH, can't we keep the assert as it is but remove "= 1" from
> "count(*) = 1" in the query. There shouldn't be more than one slot
> with same name on the primary. Or, am I missing something?

There will be 1 record max and 0 record if the primary_slot_name is
invalid. Keeping 'count(*)=1' gives the benefit that it will straight
away give us true/false indicating if we are good or not wrt
primary_slot_name. I feel Assert can be removed and we can simply
have:

        if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
                elog(ERROR, "failed to fetch primary_slot_name tuple");

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 06:18  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  5 siblings, 1 reply; 119+ messages in thread

From: Peter Smith @ 2024-01-19 06:18 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Here are some review comments for patch v63-0003.

======
Commit Message

1.
This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to 'enable_syncslot' as PGC_SIGHUP which was otherwise
a PGC_POSTMASTER GUC when slotsync worker was registered as bgworker.

~

missing word?

/allows us to/allows us to define/

======
src/backend/postmaster/postmaster.c

2. process_pm_child_exit

+ /*
+ * Was it the slot sync worker? Normal exit or FATAL exit (FATAL can
+ * be caused by libpqwalreceiver on receiving shutdown request by the
+ * startup process during promotion) can be ignored; we'll start a new
+ * one at the next iteration of the postmaster's main loop, if
+ * necessary. Any other exit condition is treated as a crash.
+ */
+ if (pid == SlotSyncWorkerPID)
+ {
+ SlotSyncWorkerPID = 0;
+ if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+ HandleChildCrash(pid, exitstatus,
+ _("Slotsync worker process"));
+ continue;
+ }

2a.

I think the 2nd sentence is easier to read if written like:

Normal exit or FATAL exit can be ignored (FATAL can be caused by
libpqwalreceiver on receiving shutdown request by the startup process
during promotion);

~

2b.
All other names nearby are lowercase so maybe change "Slotsync worker
process" to ""slotsync worker process" or ""slot sync worker process".

======
src/backend/replication/logical/slotsync.c

3. check_primary_info

  if (!valid)
- ereport(ERROR,
+ {
+ *primary_slot_invalid = true;
+ ereport(LOG,
  errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("exiting from slot synchronization due to bad configuration"),
+ errmsg("skipping slot synchronization due to bad configuration"),
  /* translator: second %s is a GUC variable name */
  errdetail("The primary server slot \"%s\" specified by %s is not valid.",
    PrimarySlotName, "primary_slot_name"));
+ }

Somehow it seems more appropriate for the *caller* to decide what to
do (e.g. "skipping...") when the primary slot is invalid. See  also
the next review comment #4b -- maybe just change this LOG to say "bad
configuration for slot synchronization".

~~~

4.
 /*
  * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * appropriately. If not, log the message and pass 'valid' as false
+ * to the caller.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
  * returns it.
  */
 static char *
-validate_parameters_and_get_dbname(void)
+validate_parameters_and_get_dbname(bool *valid)

4a.
This feels back-to-front. I think a "validate" function should return
boolean. It can return the dbname as a side-effect only when it is
valid.

SUGGESTION
static boolean
validate_parameters_and_get_dbname(char *dbname)

~

4b.
It was a bit different when there were ERRORs but now they are LOGs
somehow it seems wrong for this function to say what the *caller* will
do. Maybe you can rewrite all the errmsg so the don't say "skipping"
but they just say "bad configuration for slot synchronization"

If valid is false then you can LOG "skipping" at the caller...

~~~

5. wait_for_valid_params_and_get_dbname

+ dbname = validate_parameters_and_get_dbname(&valid);
+ if (valid)
+ break;
+ else

This code will be simpler when the function is change to return
boolean as suggested above in #4a.

Also the 'else' is unnecessary.

SUGGESTION
if (validate_parameters_and_get_dbname(&dbname)
break;
~

6.
+ if (rc & WL_LATCH_SET)
+ ResetLatch(MyLatch);
+
+ }
+ }

Unnecessary blank line.

~~~

7. slotsync_reread_config

+ if (old_enable_syncslot != enable_syncslot)
+ {
+ /*
+ * We have reached here, so old value must be true and new must be
+ * false.
+ */
+ Assert(old_enable_syncslot);
+ Assert(!enable_syncslot);

I felt it would be better just to say Assert(enable_syncslot); at the
top of this function (before the ProcessConfigFile). Then none of this
other comment/assert if really needed because it should be
self-evident.

~~~

8. StartSlotSyncWorker

int
StartSlotSyncWorker(void)
{
pid_t pid;

#ifdef EXEC_BACKEND
switch ((pid = slotsyncworker_forkexec()))
#else
switch ((pid = fork_process()))
#endif
{
case -1:
ereport(LOG,
(errmsg("could not fork slot sync worker process: %m")));
return 0;

#ifndef EXEC_BACKEND
case 0:
/* in postmaster child ... */
InitPostmasterChild();

/* Close the postmaster's sockets */
ClosePostmasterPorts(false);

ReplSlotSyncWorkerMain(0, NULL);
break;
#endif
default:
return (int) pid;
}

/* shouldn't get here */
return 0;
}

The switch code can be rearranged so you don't need the #ifndef

SUGGESTION
#ifdef EXEC_BACKEND
switch ((pid = slotsyncworker_forkexec()))
{
#else
switch ((pid = fork_process()))
{
case 0:
/* in postmaster child ... */
InitPostmasterChild();

/* Close the postmaster's sockets */
ClosePostmasterPorts(false);

ReplSlotSyncWorkerMain(0, NULL);
break;
#endif
case -1:
ereport(LOG,
(errmsg("could not fork slot sync worker process: %m")));
return 0;
default:
return (int) pid;
}

======
src/backend/storage/lmgr/proc.c

9. InitProcess

  * this; it probably should.)
+ *
+ * Slot sync worker does not participate in it, see comments atop Backend.
  */
- if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+ if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+ !IsLogicalSlotSyncWorker())
  MarkPostmasterChildActive();

9a.
/does not participate in it/also does not participate in it/

~

9b.
It's not clear where "atop Backend" is referring to.

~~~

10.
  * way, so tell the postmaster we've cleaned up acceptably well. (XXX
  * autovac launcher should be included here someday)
  */
- if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+ if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+ !IsLogicalSlotSyncWorker())
  MarkPostmasterChildInactive();

Should this comment also be updated to mention slot sync worker?

======
src/backend/utils/activity/pgstat_io.c

11. pgstat_tracks_io_bktype

  case B_WAL_SENDER:
+ case B_SLOTSYNC_WORKER:
  return true;
  }

Notice all the other enums were arrange in alphabetical order, so do
the same here.

======
src/backend/utils/init/miscinit.c

12. GetBackendTypeDesc

+ case B_SLOTSYNC_WORKER:
+ backendDesc = "slotsyncworker";
+ break;
  }
All the other case are in alphabetical order, same as the enum values,
so do the same here.

~~~

13. InitializeSessionUserIdStandalone

  * This function should only be called in single-user mode, in autovacuum
  * workers, and in background workers.
  */
- Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
IsBackgroundWorker);
+ Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+    IsLogicalSlotSyncWorker() || IsBackgroundWorker);

Looks like this Assert has a stale comment that should be updated.

======
src/include/miscadmin.h

14. GetBackendTypeDesc

  B_WAL_SUMMARIZER,
+ B_SLOTSYNC_WORKER,
  B_WAL_WRITER,
 } BackendType;

It seems strange to jam this new value among the other B_WAL enums.
Anyway, it looks like everything else is in alphabetical order, so we
do that too.

======
Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 08:12  Bertrand Drouvot <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-19 08:12 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Hi,

On Fri, Jan 19, 2024 at 11:46:51AM +0530, shveta malik wrote:
> On Fri, Jan 19, 2024 at 11:23 AM Amit Kapila <[email protected]> wrote:
> >
> > > > 5 === (coming from v62-0002)
> > > > +       Assert(tuplestore_tuple_count(res->tuplestore) == 1);
> > > >
> > > > Is it even possible for the related query to not return only one row? (I think the
> > > > "count" ensures it).
> > >
> > > I think you are right. This assertion was added sometime back on the
> > > basis of feedback on hackers. Let me review that again. I can consider
> > > this comment in the next version.
> > >
> >
> > OTOH, can't we keep the assert as it is but remove "= 1" from
> > "count(*) = 1" in the query. There shouldn't be more than one slot
> > with same name on the primary. Or, am I missing something?
> 
> There will be 1 record max and 0 record if the primary_slot_name is
> invalid. 

I think we'd have exactly one record in all the cases (due to the count):

postgres=# SELECT pg_is_in_recovery(), count(*) FROM pg_replication_slots WHERE 1 = 2;
 pg_is_in_recovery | count
-------------------+-------
 f                 |     0
(1 row)

postgres=# SELECT pg_is_in_recovery(), count(*) FROM pg_replication_slots WHERE 1 = 1;
 pg_is_in_recovery | count
-------------------+-------
 f                 |     1
(1 row)

> Keeping 'count(*)=1' gives the benefit that it will straight
> away give us true/false indicating if we are good or not wrt
> primary_slot_name. I feel Assert can be removed and we can simply
> have:
> 
>         if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
>                 elog(ERROR, "failed to fetch primary_slot_name tuple");
> 

I'd also vote for keeping it as it is and remove the Assert.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 10:25  shveta malik <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-19 10:25 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Fri, Jan 19, 2024 at 10:35 AM Masahiko Sawada <[email protected]> wrote:
>
>
> Thank you for updating the patch. I have some comments:
>
> ---
> +        latestWalEnd = GetWalRcvLatestWalEnd();
> +        if (remote_slot->confirmed_lsn > latestWalEnd)
> +        {
> +                elog(ERROR, "exiting from slot synchronization as the
> received slot sync"
> +                         " LSN %X/%X for slot \"%s\" is ahead of the
> standby position %X/%X",
> +                         LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
> +                         remote_slot->name,
> +                         LSN_FORMAT_ARGS(latestWalEnd));
> +        }
>
> IIUC GetWalRcvLatestWalEnd () returns walrcv->latestWalEnd, which is
> typically the primary server's flush position and doesn't mean the LSN
> where the walreceiver received/flushed up to.

yes. I think it makes more sense to use something which actually tells
flushed-position. I gave it a try by replacing GetWalRcvLatestWalEnd()
with GetWalRcvFlushRecPtr() but I see a problem here. Lets say I have
enabled the slot-sync feature in a running standby, in that case we
are all good (flushedUpto is the same as actual flush-position
indicated by LogstreamResult.Flush). But if I restart standby, then I
observed that the startup process sets flushedUpto to some value 'x'
(see [1]) while when the wal-receiver starts, it sets
'LogstreamResult.Flush' to another value (see [2]) which is always
greater than 'x'. And we do not update flushedUpto with the
'LogstreamResult.Flush' value in walreceiver until we actually do an
operation on primary. Performing a data change on primary sends WALs
to standby which then hits XLogWalRcvFlush() and updates flushedUpto
same as LogstreamResult.Flush. Until then we have a situation where
slots received on standby are ahead of flushedUpto and thus slotsync
worker keeps one erroring out. I am yet to find out why flushedUpto is
set to a lower value than 'LogstreamResult.Flush' at the start of
standby.  Or maybe am I using the wrong function
GetWalRcvFlushRecPtr() and should be using something else instead?

[1]:
Startup process sets 'flushedUpto' here:
ReadPageInternal-->XLogPageRead-->WaitForWALToBecomeAvailable-->RequestXLogStreaming

[2]:
Walreceiver sets 'LogstreamResult.Flush' here but do not update
'flushedUpto' here:
WalReceiverMain():  LogstreamResult.Write = LogstreamResult.Flush =
GetXLogReplayRecPtr(NULL)


> Does it really happen
> that the slot's confirmed_flush_lsn is higher than the primary's flush
> lsn?

It may happen if we have not configured standby_slot_names on primary.
In such a case, slots may get updated w/o confirming that standby has
taken the change and thus slot-sync worker may fetch the slots which
have lsns ahead of the latest WAL position on standby.

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 10:48  shveta malik <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-19 10:48 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Fri, Jan 19, 2024 at 1:42 PM Bertrand Drouvot
<[email protected]> wrote:
>
> Hi,
>
> On Fri, Jan 19, 2024 at 11:46:51AM +0530, shveta malik wrote:
> > Keeping 'count(*)=1' gives the benefit that it will straight
> > away give us true/false indicating if we are good or not wrt
> > primary_slot_name. I feel Assert can be removed and we can simply
> > have:
> >
> >         if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
> >                 elog(ERROR, "failed to fetch primary_slot_name tuple");
> >
>
> I'd also vote for keeping it as it is and remove the Assert.

Sure, retained the query as is. Removed Assert.

PFA v64. Changes are:
1) Addressed comments by Amit in [1].
2) Addressed offlist comments given by Peter for documentation patch06.
3) Moved some docs to patch04 which were wrongly placed in patch02.
4) Addressed 1 pending comment from Bertrand (as stated above) to
remove redundant Assert from check_primary_info()

TODO:
Address comments by Peter given in [2]

[1]: https://www.postgresql.org/message-id/CAA4eK1LBnCjxBi7vPam0OfxsTEyHdvqx7goKxi1ePU45oz%3Dkhg%40mail.g...
[2]: https://www.postgresql.org/message-id/CAHut%2BPt5Pk_xJkb54oahR%2Bf9oawgfnmbpewvkZPgnRhoJ3gkYg%40mail...

thanks
Shveta


Attachments:

  [application/octet-stream] v64-0003-Slot-sync-worker-as-a-special-process.patch (35.8K, ../../CAJpy0uBVDTbiLweRNMt53qMSOnPObocwDVhAdvsNCsNRQ3aNhA@mail.gmail.com/2-v64-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From 01ab164cff8b1629883bb8c50b24ad20c4623c4d Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Wed, 17 Jan 2024 13:56:25 +0530
Subject: [PATCH v64 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to 'enable_syncslot' as PGC_SIGHUP which was otherwise
a PGC_POSTMASTER GUC when slotsync worker was registered as bgworker.
---
 doc/src/sgml/bgworker.sgml                 |  65 +----
 src/backend/postmaster/bgworker.c          |   3 -
 src/backend/postmaster/postmaster.c        |  85 ++++--
 src/backend/replication/logical/slotsync.c | 323 ++++++++++++++++-----
 src/backend/storage/lmgr/proc.c            |   9 +-
 src/backend/tcop/postgres.c                |  11 -
 src/backend/utils/activity/pgstat_io.c     |   1 +
 src/backend/utils/init/miscinit.c          |   7 +-
 src/backend/utils/init/postinit.c          |   8 +-
 src/backend/utils/misc/guc_tables.c        |   2 +-
 src/include/miscadmin.h                    |   1 +
 src/include/postmaster/bgworker.h          |   1 -
 src/include/replication/worker_internal.h  |   7 +-
 13 files changed, 350 insertions(+), 173 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..471ee5eccc 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,9 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY)
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1015,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1835,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2686,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3045,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3217,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit (FATAL can
+		 * be caused by libpqwalreceiver on receiving shutdown request by the
+		 * startup process during promotion) can be ignored; we'll start a new
+		 * one at the next iteration of the postmaster's main loop, if
+		 * necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("Slotsync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3599,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3745,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3762,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3778,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3877,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4101,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4915,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5019,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5848,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 844b1a4def..9ffdf5d3ba 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -27,6 +27,8 @@
  * It waits for a period of time before the next synchronization, with the
  * duration varying based on whether any slots were updated during the last
  * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
  *---------------------------------------------------------------------------
  */
 
@@ -40,7 +42,9 @@
 #include "commands/dbcommands.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -53,6 +57,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -97,13 +103,24 @@ SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
 /* GUC variable */
 bool		enable_syncslot = false;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 /*
  * Sleep time in ms between slot-sync cycles.
  * See wait_for_slot_activity() for how we adjust this
  */
 static long sleep_ms;
 
+/* Min and Max sleep time for slot sync worker */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
 
 /*
  * If necessary, update local slot metadata based on the data from the remote
@@ -689,7 +706,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -704,8 +722,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -741,12 +761,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("skipping slot synchronization due to bad configuration"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -756,18 +779,18 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 
 /*
  * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * appropriately. If not, log the message and pass 'valid' as false
+ * to the caller.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
  * returns it.
  */
 static char *
-validate_parameters_and_get_dbname(void)
+validate_parameters_and_get_dbname(bool *valid)
 {
 	char	   *dbname;
 
-	/* Sanity check. */
-	Assert(enable_syncslot);
+	*valid = false;
 
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
@@ -776,10 +799,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_slot_name"));
+		return NULL;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -787,30 +813,39 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be enabled.", "hot_standby_feedback"));
+		return NULL;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("wal_level must be >= logical."));
+		return NULL;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_conninfo"));
+		return NULL;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
@@ -818,14 +853,61 @@ validate_parameters_and_get_dbname(void)
 	 */
 	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
 	if (dbname == NULL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+		return NULL;
+	}
+
+	/* All good, set valid to true now */
+	*valid = true;
+
+	return dbname;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+	bool		valid;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		dbname = validate_parameters_and_get_dbname(&valid);
+		if (valid)
+			break;
+		else
+		{
+			ProcessSlotSyncInterrupts(NULL);
+
+			rc = WaitLatch(MyLatch,
+						   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+						   MAX_WORKER_NAPTIME_MS,
+						   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+			if (rc & WL_LATCH_SET)
+				ResetLatch(MyLatch);
+
+		}
+	}
 
 	return dbname;
 }
@@ -841,6 +923,7 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
@@ -851,6 +934,22 @@ slotsync_reread_config(void)
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		/*
+		 * We have reached here, so old value must be true and new must be
+		 * false.
+		 */
+		Assert(old_enable_syncslot);
+		Assert(!enable_syncslot);
+
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -858,8 +957,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of"
 					   " a parameter change"));
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -876,7 +974,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down"
 					   " on receiving SIGINT"));
@@ -909,20 +1008,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
-#define MIN_WORKER_NAPTIME_MS  200
-#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
-
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -958,20 +1053,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
+
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
 
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -986,26 +1099,69 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	BackgroundWorkerUnblockSignals();
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1024,26 +1180,27 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1060,7 +1217,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1134,42 +1291,64 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("enable_syncslot is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
+
+	Assert(ac < lengthof(av));
+
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	memset(&bgw, 0, sizeof(bgw));
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+#else
+	switch ((pid = fork_process()))
+#endif
+	{
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+#ifndef EXEC_BACKEND
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..ad1352bf76 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,11 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker does not participate in it, see comments atop Backend.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -935,7 +939,8 @@ ProcKill(int code, Datum arg)
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..c5de528b34 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -341,6 +341,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
+		case B_SLOTSYNC_WORKER:
 			return true;
 	}
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..d5e16f1df4 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -311,6 +312,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_WAL_WRITER:
 			backendDesc = "walwriter";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 	}
 
 	return backendDesc;
@@ -837,7 +841,8 @@ InitializeSessionUserIdStandalone(void)
 	 * This function should only be called in single-user mode, in autovacuum
 	 * workers, and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0f5ec63de1..5763454336 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2046,7 +2046,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..e89b8121f3 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -337,6 +337,7 @@ typedef enum BackendType
 	B_WAL_RECEIVER,
 	B_WAL_SENDER,
 	B_WAL_SUMMARIZER,
+	B_SLOTSYNC_WORKER,
 	B_WAL_WRITER,
 } BackendType;
 
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..cd530d3d40 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,10 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v64-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uBVDTbiLweRNMt53qMSOnPObocwDVhAdvsNCsNRQ3aNhA@mail.gmail.com/3-v64-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 94c9e25ca51a324dd831168209d1b40625d277f2 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 16:22:05 +0530
Subject: [PATCH v64 5/6] Non replication connection and app_name change.

Changes in this patch:
1) Convert replication connection to non-replication one in slotsync worker.
2) Use app_name as {cluster_name}_slotsyncworker in the slotsync worker
connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index f50be29d99..13da6b1466 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1530,7 +1532,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1781,7 +1784,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 934c1a3ab0..d6f0cae0a9 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -50,7 +53,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -125,7 +129,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -136,8 +145,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	const char *keys[6];
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 9ffdf5d3ba..1e0f0ac3cd 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1062,6 +1062,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1163,13 +1164,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 3dea10f9b3..95e7324c8f 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index ffacd55e5c..cfe647ec58 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 5e942cb4fc..68ac074274 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -434,8 +435,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v64-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.7K, ../../CAJpy0uBVDTbiLweRNMt53qMSOnPObocwDVhAdvsNCsNRQ3aNhA@mail.gmail.com/4-v64-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From caa01380af62612f7a0e34e26ab41128d7abf046 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:02:42 +0530
Subject: [PATCH v64 4/6] Allow logical walsenders to wait for the physical
 standbys

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 738 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 33f957b02f..d0509fb5a5 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2210,3 +2224,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index c81a7a8344..acf562fe93 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3527,6 +3599,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3596,8 +3669,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0879bab57e..fead31748e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5763454336..08db4c37bc 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4618,6 +4618,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 6830f7d16b..0c7b6117e0 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f81bef9e42..eef9f25c45 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..9a42b01f9a 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -45,6 +45,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index c17abfb40b..d50bbb3ae7 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v64-0001-Enable-setting-failover-property-for-a-slot-thro.patch (100.3K, ../../CAJpy0uBVDTbiLweRNMt53qMSOnPObocwDVhAdvsNCsNRQ3aNhA@mail.gmail.com/5-v64-0001-Enable-setting-failover-property-for-a-slot-thro.patch)
  download | inline diff:
From 43857b71597860c8e30ba10fb25ee352d0ed78b5 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 4 Jan 2024 09:15:26 +0530
Subject: [PATCH v64 1/6] Enable setting failover property for a slot through
 SQL API and subscription commands

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the next patch will
address that.

In addition, a new replication command named ALTER_REPLICATION_SLOT and a
corresponding walreceiver API function named walrcv_alter_slot have been
implemented. These additions provide subscribers or users the ability to
modify the failover property of a replication slot on the publisher.

Moreover, a new subscription option called 'failover' has been added,
allowing users to set it when creating or altering a subscription. Also,
a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out       |  58 ++++++
 contrib/test_decoding/sql/slot.sql            |  13 ++
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/func.sgml                        |  11 +-
 doc/src/sgml/protocol.sgml                    |  52 ++++++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 doc/src/sgml/system-views.sgml                |  11 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_functions.sql      |   1 +
 src/backend/catalog/system_views.sql          |   6 +-
 src/backend/commands/subscriptioncmds.c       | 105 ++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  38 +++-
 src/backend/replication/logical/tablesync.c   |   1 +
 src/backend/replication/logical/worker.c      |   7 +
 src/backend/replication/repl_gram.y           |  20 ++-
 src/backend/replication/repl_scanner.l        |   2 +
 src/backend/replication/slot.c                |  33 +++-
 src/backend/replication/slotfuncs.c           |  22 ++-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |  64 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/info.c                     |   5 +-
 src/bin/pg_upgrade/pg_upgrade.c               |   6 +-
 src/bin/pg_upgrade/pg_upgrade.h               |   2 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_proc.dat               |  14 +-
 src/include/catalog/pg_subscription.h         |  11 ++
 src/include/nodes/replnodes.h                 |  12 ++
 src/include/replication/slot.h                |   9 +-
 src/include/replication/walreceiver.h         |  18 +-
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 39 files changed, 745 insertions(+), 124 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..f224327c00 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the physical standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..154c426df7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        physical standbys so that logical replication can be resumed
+        after failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..af997efd2b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..ce386a46a7 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the physical standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..1868b95836 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the physical
+       standbys so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
@@ -1357,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..f50be29d99 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,30 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set failover for a subscription that does not have a slot name")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified if
+					 * the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1512,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error
+	 * while doing the database operations we won't be able to rollback
+	 * altered slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 201c36cb22..c5232cee2f 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -74,8 +74,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -96,6 +99,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -899,8 +903,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -929,7 +933,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -998,6 +1003,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 911835c5cb..3dea10f9b3 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..696376400e 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to physical standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -311,6 +314,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -679,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, true);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..c93dba855b 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -133,7 +135,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +174,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +192,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +431,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -782,11 +789,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e00395ff2b..ffacd55e5c 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,13 +1217,14 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 6bfdb5f008..c4aebd13b1 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 58811a6530..b9e747d72f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11115,17 +11115,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ca32625585..c92a81e6b7 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,12 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -145,6 +151,11 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..585ccbb504 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for physical standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,9 +224,10 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 17c0104376..b39a7d8cdf 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3873,6 +3874,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v64-0002-Add-logical-slot-sync-capability-to-the-physical.patch (83.4K, ../../CAJpy0uBVDTbiLweRNMt53qMSOnPObocwDVhAdvsNCsNRQ3aNhA@mail.gmail.com/6-v64-0002-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From e917074752ba8010a192df24655876ba16855cb7 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Fri, 12 Jan 2024 20:41:19 +0800
Subject: [PATCH v64 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1175 +++++++++++++++++
 src/backend/replication/slot.c                |   32 +-
 src/backend/replication/slotfuncs.c           |   14 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |    4 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    2 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   19 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 33 files changed, 1697 insertions(+), 37 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1868b95836..4f36a2f732 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index c5232cee2f..934c1a3ab0 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -34,6 +34,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -100,6 +102,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -432,6 +435,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..844b1a4def
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1175 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * Sleep time in ms between slot-sync cycles.
+ * See wait_for_slot_activity() for how we adjust this
+ */
+static long sleep_ms;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			remote_dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	remote_dbid = get_database_oid(remote_slot->database, false);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	SpinLockAcquire(&slot->mutex);
+	namestrcpy(&slot->data.plugin, remote_slot->plugin);
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	Assert(local_slot_update(remote_slot));
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly locally created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	if (remote_slot->confirmed_lsn > latestWalEnd)
+	{
+		elog(ERROR, "exiting from slot synchronization as the received slot sync"
+			 " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+			 LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+			 remote_slot->name,
+			 LSN_FORMAT_ARGS(latestWalEnd));
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on standby",
+						   remote_slot->name),
+					errdetail("A user-created slot with the same name as"
+							  " failover slot already exists on the standby."));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(remote_slot->database, false);
+		namestrcpy(&slot->data.plugin, remote_slot->plugin);
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info"
+					   " from the primary server: %s", res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, 7, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 8, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 9, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the"
+					   " primary server: %s", PrimarySlotName, res->err));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR, "failed to fetch primary_slot_name tuple");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("wal_level must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of"
+					   " a parameter change"));
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down"
+					   " on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for logical replication launcher.
+ *
+ * Called on logical replication launcher exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the logical replication launcher, or when there
+	 * is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("enable_syncslot is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 696376400e..33f957b02f 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to physical standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -315,6 +316,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +682,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +711,16 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	/*
+	 * Do not allow users to alter the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot alter replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +733,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +889,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index c93dba855b..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..c81a7a8344 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index e5119ed55d..04fed1007e 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
@@ -342,6 +343,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index f625473ad4..0879bab57e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,8 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_PRIMARY_CATCHUP	"Waiting for the primary to catch-up, in slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index e53ebc6dc2..0f5ec63de1 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2044,6 +2045,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 835b0e9ba8..6830f7d16b 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b9e747d72f..6dc234f9f7 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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 585ccbb504..f81bef9e42 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for physical standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..5e942cb4fc 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,21 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbinfo_for_failover_slots_fn
+ *
+ * Run LIST_DBID_FOR_FAILOVER_SLOTS on primary server to get the
+ * list of unique DBIDs for failover logical slots
+ */
+typedef List *(*walrcv_get_dbinfo_for_failover_slots_fn) (WalReceiverConn *conn);
+
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +418,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +444,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +503,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..c17abfb40b 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly locally created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT failover, synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t|t",
+	'logical slot has failover as true and synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 271313ebf8..aac83755de 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -132,8 +132,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(22 rows)
+(23 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b39a7d8cdf..79137329e0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2320,6 +2320,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2580,6 +2581,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v64-0006-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../CAJpy0uBVDTbiLweRNMt53qMSOnPObocwDVhAdvsNCsNRQ3aNhA@mail.gmail.com/7-v64-0006-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From 7c1f5a5c3c2bd0d1a162d71ec1f361471d1703f0 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v64 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 10:55  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-19 10:55 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Thu, Jan 18, 2024 at 4:49 PM Amit Kapila <[email protected]> wrote:

> 2.
> +synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
> {
> ...
> + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
> + xmin_horizon = GetOldestSafeDecodingTransactionId(true);
> + SpinLockAcquire(&slot->mutex);
> + slot->data.catalog_xmin = xmin_horizon;
> + SpinLockRelease(&slot->mutex);
> ...
> }
>
> Here, why slot->effective_catalog_xmin is not updated? The same is
> required by a later call to ReplicationSlotsComputeRequiredXmin(). I
> see that the prior version v60-0002 has the corresponding change but
> it is missing in the latest version. Any reason?

I think it was a mistake in v61. Added it back in v64..

>
> 3.
> + * Return true either if the slot is marked as RS_PERSISTENT (sync-ready) or
> + * is synced periodically (if it was already sync-ready). Return false
> + * otherwise.
> + */
> +static bool
> +update_and_persist_slot(RemoteSlot *remote_slot)
>
> The second part of the above comment (or is synced periodically (if it
> was already sync-ready)) is not clear to me. Does it intend to
> describe the case when we try to update the already created temp slot
> in the last call. If so, that is not very clear because periodically
> sounds like it can be due to repeated sync for sync-ready slot.

The comment was as per old functionality where this function was doing
persist and save both. In v61 code changed, but comment was not
updated. I have changed it now in v64.

> 4.
> +update_and_persist_slot(RemoteSlot *remote_slot)
> {
> ...
> + (void) local_slot_update(remote_slot);
> ...
> }
>
> Can we write a comment to state the reason why we don't care about the
> return value here?

Since it is the first time 'local_slot_update' is happening on any
slot, the return value must be true i.e. local_slot_update() should
not skip the update. I have thus added an Assert on return value now
(in v64).

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-19 11:53  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  5 siblings, 3 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-19 11:53 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
>

I had some off-list discussions with Sawada-San, Hou-San, and Shveta
on the topic of extending replication commands instead of using the
current model where we fetch the required slot information via SQL
using a database connection. I would like to summarize the discussion
and would like to know the thoughts of others on this topic.

In the current patch, we launch the slotsync worker on physical
standby which connects to the specified database (currently we let
users specify the required dbname in primary_conninfo) on the primary.
It then fetches the required information for failover marked slots
from the primary and also does some primitive checks on the upstream
node via SQL (the additional checks are like whether the upstream node
has a specified physical slot or whether the upstream node is a
primary node or a standby node). To fetch the required information it
uses a libpqwalreciever API which is mostly apt for this purpose as it
supports SQL execution but for this patch, we don't need a replication
connection, so we extend the libpqwalreciever connect API.

Now, the concerns related to this could be that users would probably
need to change existing mechanisms/tools to update priamry_conninfo
and one of the alternatives proposed is to have an additional GUC like
slot_sync_dbname. Users won't be able to drop the database this worker
is connected to aka whatever is specified in slot_sync_dbname but as
the user herself sets up the configuration it shouldn't be a big deal.
Then we also discussed whether extending libpqwalreceiver's connect
API is a good idea and whether we need to further extend it in the
future. As far as I can see, slotsync worker's primary requirement is
to execute SQL queries which the current API is sufficient, and don't
see something that needs any drastic change in this API. Note that
tablesync worker that executes SQL also uses these APIs, so we may
need something in the future for either of those. Then finally we need
a slotsync worker to also connect to a database to use SQL and fetch
results.

Now, let us consider if we extend the replication commands like
READ_REPLICATION_SLOT and or introduce a new set of replication
commands to fetch the required information then we don't need a DB
connection with primary or a connection in slotsync worker. As per my
current understanding, it is quite doable but I think we will slowly
go in the direction of making replication commands something like SQL
because today we need to extend it to fetch all slots info that have
failover marked as true, the existence of a particular replication,
etc. Then tomorrow, if we want to extend this work to have multiple
slotsync workers say workers perdb then we have to extend the
replication command to fetch per-database failover marked slots. To
me, it sounds more like we are slowly adding SQL-like features to
replication commands.

Apart from this when we are reading per-db replication slots without
connecting to a database, we probably need some additional protection
mechanism so that the database won't get dropped.

Considering all this it seems that for now probably extending
replication commands can simplify a few things like mentioned above
but using SQL's with db-connection is more extendable.

Thoughts?

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-20 05:21  Dilip Kumar <[email protected]>
  parent: Amit Kapila <[email protected]>
  2 siblings, 1 reply; 119+ messages in thread

From: Dilip Kumar @ 2024-01-20 05:21 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Fri, Jan 19, 2024 at 5:24 PM Amit Kapila <[email protected]> wrote:
>
> On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
> >
>
> I had some off-list discussions with Sawada-San, Hou-San, and Shveta
> on the topic of extending replication commands instead of using the
> current model where we fetch the required slot information via SQL
> using a database connection. I would like to summarize the discussion
> and would like to know the thoughts of others on this topic.
>
> In the current patch, we launch the slotsync worker on physical
> standby which connects to the specified database (currently we let
> users specify the required dbname in primary_conninfo) on the primary.
> It then fetches the required information for failover marked slots
> from the primary and also does some primitive checks on the upstream
> node via SQL (the additional checks are like whether the upstream node
> has a specified physical slot or whether the upstream node is a
> primary node or a standby node). To fetch the required information it
> uses a libpqwalreciever API which is mostly apt for this purpose as it
> supports SQL execution but for this patch, we don't need a replication
> connection, so we extend the libpqwalreciever connect API.

What sort of extension we have done to 'libpqwalreciever'? Is it
something like by default this supports replication connections so we
have done an extension to the API so that we can provide an option
whether to create a replication connection or a normal connection?

> Now, the concerns related to this could be that users would probably
> need to change existing mechanisms/tools to update priamry_conninfo
> and one of the alternatives proposed is to have an additional GUC like
> slot_sync_dbname. Users won't be able to drop the database this worker
> is connected to aka whatever is specified in slot_sync_dbname but as
> the user herself sets up the configuration it shouldn't be a big deal.

Yeah for this purpose users may use template1 or so which they
generally don't plan to drop.  So in case the user wants to drop that
database user needs to turn off the slot syncing option and then it
can be done?

> Then we also discussed whether extending libpqwalreceiver's connect
> API is a good idea and whether we need to further extend it in the
> future. As far as I can see, slotsync worker's primary requirement is
> to execute SQL queries which the current API is sufficient, and don't
> see something that needs any drastic change in this API. Note that
> tablesync worker that executes SQL also uses these APIs, so we may
> need something in the future for either of those. Then finally we need
> a slotsync worker to also connect to a database to use SQL and fetch
> results.

While looking into the patch v64-0002 I could not exactly point out
what sort of extensions are there in libpqwalreceiver.c, I just saw
one extra API for fetching the dbname from connection info?

> Now, let us consider if we extend the replication commands like
> READ_REPLICATION_SLOT and or introduce a new set of replication
> commands to fetch the required information then we don't need a DB
> connection with primary or a connection in slotsync worker. As per my
> current understanding, it is quite doable but I think we will slowly
> go in the direction of making replication commands something like SQL
> because today we need to extend it to fetch all slots info that have
> failover marked as true, the existence of a particular replication,
> etc. Then tomorrow, if we want to extend this work to have multiple
> slotsync workers say workers perdb then we have to extend the
> replication command to fetch per-database failover marked slots. To
> me, it sounds more like we are slowly adding SQL-like features to
> replication commands.
>
> Apart from this when we are reading per-db replication slots without
> connecting to a database, we probably need some additional protection
> mechanism so that the database won't get dropped.

Something like locking the database only while fetching the slots?

> Considering all this it seems that for now probably extending
> replication commands can simplify a few things like mentioned above
> but using SQL's with db-connection is more extendable.

Even I have similar thoughts.

-- 
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-20 10:44  Amit Kapila <[email protected]>
  parent: Dilip Kumar <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-20 10:44 UTC (permalink / raw)
  To: Dilip Kumar <[email protected]>; +Cc: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Sat, Jan 20, 2024 at 10:52 AM Dilip Kumar <[email protected]> wrote:
>
> On Fri, Jan 19, 2024 at 5:24 PM Amit Kapila <[email protected]> wrote:
> >
> > On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
> > >
> >
> > I had some off-list discussions with Sawada-San, Hou-San, and Shveta
> > on the topic of extending replication commands instead of using the
> > current model where we fetch the required slot information via SQL
> > using a database connection. I would like to summarize the discussion
> > and would like to know the thoughts of others on this topic.
> >
> > In the current patch, we launch the slotsync worker on physical
> > standby which connects to the specified database (currently we let
> > users specify the required dbname in primary_conninfo) on the primary.
> > It then fetches the required information for failover marked slots
> > from the primary and also does some primitive checks on the upstream
> > node via SQL (the additional checks are like whether the upstream node
> > has a specified physical slot or whether the upstream node is a
> > primary node or a standby node). To fetch the required information it
> > uses a libpqwalreciever API which is mostly apt for this purpose as it
> > supports SQL execution but for this patch, we don't need a replication
> > connection, so we extend the libpqwalreciever connect API.
>
> What sort of extension we have done to 'libpqwalreciever'? Is it
> something like by default this supports replication connections so we
> have done an extension to the API so that we can provide an option
> whether to create a replication connection or a normal connection?
>

Yeah and in the future there could be more as well. The other function
added walrcv_get_dbname_from_conninfo doesn't appear to be a problem
either for now.

> > Now, the concerns related to this could be that users would probably
> > need to change existing mechanisms/tools to update priamry_conninfo
> > and one of the alternatives proposed is to have an additional GUC like
> > slot_sync_dbname. Users won't be able to drop the database this worker
> > is connected to aka whatever is specified in slot_sync_dbname but as
> > the user herself sets up the configuration it shouldn't be a big deal.
>
> Yeah for this purpose users may use template1 or so which they
> generally don't plan to drop.
>

Using template1 has other problems like users won't be able to create
a new database. See [2] (point number 2.2)

>
>  So in case the user wants to drop that
> database user needs to turn off the slot syncing option and then it
> can be done?
>

Right.

> > Then we also discussed whether extending libpqwalreceiver's connect
> > API is a good idea and whether we need to further extend it in the
> > future. As far as I can see, slotsync worker's primary requirement is
> > to execute SQL queries which the current API is sufficient, and don't
> > see something that needs any drastic change in this API. Note that
> > tablesync worker that executes SQL also uses these APIs, so we may
> > need something in the future for either of those. Then finally we need
> > a slotsync worker to also connect to a database to use SQL and fetch
> > results.
>
> While looking into the patch v64-0002 I could not exactly point out
> what sort of extensions are there in libpqwalreceiver.c, I just saw
> one extra API for fetching the dbname from connection info?
>

Right, the worry was that we may need it in the future.

> > Now, let us consider if we extend the replication commands like
> > READ_REPLICATION_SLOT and or introduce a new set of replication
> > commands to fetch the required information then we don't need a DB
> > connection with primary or a connection in slotsync worker. As per my
> > current understanding, it is quite doable but I think we will slowly
> > go in the direction of making replication commands something like SQL
> > because today we need to extend it to fetch all slots info that have
> > failover marked as true, the existence of a particular replication,
> > etc. Then tomorrow, if we want to extend this work to have multiple
> > slotsync workers say workers perdb then we have to extend the
> > replication command to fetch per-database failover marked slots. To
> > me, it sounds more like we are slowly adding SQL-like features to
> > replication commands.
> >
> > Apart from this when we are reading per-db replication slots without
> > connecting to a database, we probably need some additional protection
> > mechanism so that the database won't get dropped.
>
> Something like locking the database only while fetching the slots?
>

Possible, but can we lock the database from an auxiliary process?

> > Considering all this it seems that for now probably extending
> > replication commands can simplify a few things like mentioned above
> > but using SQL's with db-connection is more extendable.
>
> Even I have similar thoughts.
>

Thanks.

[1] - https://www.postgresql.org/message-id/CAJpy0uBhPx1MDHh903XpFAhpBH23KzVXyg_4VjH2zXk81oGi1w%40mail.gma...

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 03:36  shveta malik <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-22 03:36 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Fri, Jan 19, 2024 at 4:18 PM shveta malik <[email protected]> wrote:
>
> PFA v64.

V64 fails to apply to HEAD due to a recent commit. Rebased it. PFA
v64_2. It has no new changes.

thanks
Shveta


Attachments:

  [application/octet-stream] v64_2-0002-Add-logical-slot-sync-capability-to-the-physic.patch (83.4K, ../../CAJpy0uDK=DfChioOaHon3LGZ7UusVLzzv_nYpoZVrzHX08Jdgw@mail.gmail.com/2-v64_2-0002-Add-logical-slot-sync-capability-to-the-physic.patch)
  download | inline diff:
From 594f116426cee8f67c6a5d6caa495e8933335fe2 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Mon, 22 Jan 2024 08:57:53 +0530
Subject: [PATCH v64_2 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1175 +++++++++++++++++
 src/backend/replication/slot.c                |   32 +-
 src/backend/replication/slotfuncs.c           |   14 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |    4 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    2 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   19 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 33 files changed, 1697 insertions(+), 37 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1868b95836..4f36a2f732 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index c5232cee2f..934c1a3ab0 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -34,6 +34,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -100,6 +102,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -432,6 +435,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..844b1a4def
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1175 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * Sleep time in ms between slot-sync cycles.
+ * See wait_for_slot_activity() for how we adjust this
+ */
+static long sleep_ms;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			remote_dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	remote_dbid = get_database_oid(remote_slot->database, false);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	SpinLockAcquire(&slot->mutex);
+	namestrcpy(&slot->data.plugin, remote_slot->plugin);
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	Assert(local_slot_update(remote_slot));
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly locally created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	if (remote_slot->confirmed_lsn > latestWalEnd)
+	{
+		elog(ERROR, "exiting from slot synchronization as the received slot sync"
+			 " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+			 LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+			 remote_slot->name,
+			 LSN_FORMAT_ARGS(latestWalEnd));
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on standby",
+						   remote_slot->name),
+					errdetail("A user-created slot with the same name as"
+							  " failover slot already exists on the standby."));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(remote_slot->database, false);
+		namestrcpy(&slot->data.plugin, remote_slot->plugin);
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info"
+					   " from the primary server: %s", res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, 7, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 8, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 9, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the"
+					   " primary server: %s", PrimarySlotName, res->err));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR, "failed to fetch primary_slot_name tuple");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("wal_level must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of"
+					   " a parameter change"));
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down"
+					   " on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for logical replication launcher.
+ *
+ * Called on logical replication launcher exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the logical replication launcher, or when there
+	 * is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("enable_syncslot is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 696376400e..33f957b02f 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to physical standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -315,6 +316,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +682,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +711,16 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	/*
+	 * Do not allow users to alter the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot alter replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +733,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +889,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index c93dba855b..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..c81a7a8344 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3069d8790e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,8 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_PRIMARY_CATCHUP	"Waiting for the primary to catch-up, in slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e6e4bbdfb9..10e6a1e951 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,9 +11123,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 585ccbb504..f81bef9e42 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for physical standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..5e942cb4fc 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,21 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbinfo_for_failover_slots_fn
+ *
+ * Run LIST_DBID_FOR_FAILOVER_SLOTS on primary server to get the
+ * list of unique DBIDs for failover logical slots
+ */
+typedef List *(*walrcv_get_dbinfo_for_failover_slots_fn) (WalReceiverConn *conn);
+
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +418,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +444,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +503,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..c17abfb40b 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly locally created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT failover, synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t|t",
+	'logical slot has failover as true and synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 513c7702ff..d527bf918b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2585,6 +2586,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v64_2-0003-Slot-sync-worker-as-a-special-process.patch (35.8K, ../../CAJpy0uDK=DfChioOaHon3LGZ7UusVLzzv_nYpoZVrzHX08Jdgw@mail.gmail.com/3-v64_2-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From b6bc11965b2e55d691f2e5240d51d928150b11ae Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Wed, 17 Jan 2024 13:56:25 +0530
Subject: [PATCH v64_2 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to 'enable_syncslot' as PGC_SIGHUP which was otherwise
a PGC_POSTMASTER GUC when slotsync worker was registered as bgworker.
---
 doc/src/sgml/bgworker.sgml                 |  65 +----
 src/backend/postmaster/bgworker.c          |   3 -
 src/backend/postmaster/postmaster.c        |  85 ++++--
 src/backend/replication/logical/slotsync.c | 323 ++++++++++++++++-----
 src/backend/storage/lmgr/proc.c            |   9 +-
 src/backend/tcop/postgres.c                |  11 -
 src/backend/utils/activity/pgstat_io.c     |   1 +
 src/backend/utils/init/miscinit.c          |   7 +-
 src/backend/utils/init/postinit.c          |   8 +-
 src/backend/utils/misc/guc_tables.c        |   2 +-
 src/include/miscadmin.h                    |   1 +
 src/include/postmaster/bgworker.h          |   1 -
 src/include/replication/worker_internal.h  |   7 +-
 13 files changed, 350 insertions(+), 173 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..471ee5eccc 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,9 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY)
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1015,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1835,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2686,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3045,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3217,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit (FATAL can
+		 * be caused by libpqwalreceiver on receiving shutdown request by the
+		 * startup process during promotion) can be ignored; we'll start a new
+		 * one at the next iteration of the postmaster's main loop, if
+		 * necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("Slotsync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3599,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3745,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3762,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3778,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3877,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4101,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4915,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5019,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5848,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 844b1a4def..9ffdf5d3ba 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -27,6 +27,8 @@
  * It waits for a period of time before the next synchronization, with the
  * duration varying based on whether any slots were updated during the last
  * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
  *---------------------------------------------------------------------------
  */
 
@@ -40,7 +42,9 @@
 #include "commands/dbcommands.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -53,6 +57,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -97,13 +103,24 @@ SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
 /* GUC variable */
 bool		enable_syncslot = false;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 /*
  * Sleep time in ms between slot-sync cycles.
  * See wait_for_slot_activity() for how we adjust this
  */
 static long sleep_ms;
 
+/* Min and Max sleep time for slot sync worker */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
 
 /*
  * If necessary, update local slot metadata based on the data from the remote
@@ -689,7 +706,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -704,8 +722,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -741,12 +761,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("skipping slot synchronization due to bad configuration"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -756,18 +779,18 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 
 /*
  * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * appropriately. If not, log the message and pass 'valid' as false
+ * to the caller.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
  * returns it.
  */
 static char *
-validate_parameters_and_get_dbname(void)
+validate_parameters_and_get_dbname(bool *valid)
 {
 	char	   *dbname;
 
-	/* Sanity check. */
-	Assert(enable_syncslot);
+	*valid = false;
 
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
@@ -776,10 +799,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_slot_name"));
+		return NULL;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -787,30 +813,39 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be enabled.", "hot_standby_feedback"));
+		return NULL;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("wal_level must be >= logical."));
+		return NULL;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("%s must be defined.", "primary_conninfo"));
+		return NULL;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
@@ -818,14 +853,61 @@ validate_parameters_and_get_dbname(void)
 	 */
 	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
 	if (dbname == NULL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("skipping slot synchronization due to bad configuration"),
 				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+		return NULL;
+	}
+
+	/* All good, set valid to true now */
+	*valid = true;
+
+	return dbname;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+	bool		valid;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		dbname = validate_parameters_and_get_dbname(&valid);
+		if (valid)
+			break;
+		else
+		{
+			ProcessSlotSyncInterrupts(NULL);
+
+			rc = WaitLatch(MyLatch,
+						   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+						   MAX_WORKER_NAPTIME_MS,
+						   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+			if (rc & WL_LATCH_SET)
+				ResetLatch(MyLatch);
+
+		}
+	}
 
 	return dbname;
 }
@@ -841,6 +923,7 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
@@ -851,6 +934,22 @@ slotsync_reread_config(void)
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		/*
+		 * We have reached here, so old value must be true and new must be
+		 * false.
+		 */
+		Assert(old_enable_syncslot);
+		Assert(!enable_syncslot);
+
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -858,8 +957,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of"
 					   " a parameter change"));
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -876,7 +974,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down"
 					   " on receiving SIGINT"));
@@ -909,20 +1008,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
-#define MIN_WORKER_NAPTIME_MS  200
-#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
-
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -958,20 +1053,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
+
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
 
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -986,26 +1099,69 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	BackgroundWorkerUnblockSignals();
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1024,26 +1180,27 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1060,7 +1217,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1134,42 +1291,64 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("enable_syncslot is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
+
+	Assert(ac < lengthof(av));
+
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	memset(&bgw, 0, sizeof(bgw));
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+#else
+	switch ((pid = fork_process()))
+#endif
+	{
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+#ifndef EXEC_BACKEND
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..ad1352bf76 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,11 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker does not participate in it, see comments atop Backend.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -935,7 +939,8 @@ ProcKill(int code, Datum arg)
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..c5de528b34 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -341,6 +341,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
+		case B_SLOTSYNC_WORKER:
 			return true;
 	}
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..d5e16f1df4 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -311,6 +312,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_WAL_WRITER:
 			backendDesc = "walwriter";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 	}
 
 	return backendDesc;
@@ -837,7 +841,8 @@ InitializeSessionUserIdStandalone(void)
 	 * This function should only be called in single-user mode, in autovacuum
 	 * workers, and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..e89b8121f3 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -337,6 +337,7 @@ typedef enum BackendType
 	B_WAL_RECEIVER,
 	B_WAL_SENDER,
 	B_WAL_SUMMARIZER,
+	B_SLOTSYNC_WORKER,
 	B_WAL_WRITER,
 } BackendType;
 
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..cd530d3d40 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,10 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v64_2-0001-Enable-setting-failover-property-for-a-slot-th.patch (100.3K, ../../CAJpy0uDK=DfChioOaHon3LGZ7UusVLzzv_nYpoZVrzHX08Jdgw@mail.gmail.com/4-v64_2-0001-Enable-setting-failover-property-for-a-slot-th.patch)
  download | inline diff:
From ab734fcf4aeca1b7f3a36d6ee3e3a582aef3ad3c Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 4 Jan 2024 09:15:26 +0530
Subject: [PATCH v64_2 1/6] Enable setting failover property for a slot through
 SQL API and subscription commands

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the next patch will
address that.

In addition, a new replication command named ALTER_REPLICATION_SLOT and a
corresponding walreceiver API function named walrcv_alter_slot have been
implemented. These additions provide subscribers or users the ability to
modify the failover property of a replication slot on the publisher.

Moreover, a new subscription option called 'failover' has been added,
allowing users to set it when creating or altering a subscription. Also,
a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out       |  58 ++++++
 contrib/test_decoding/sql/slot.sql            |  13 ++
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/func.sgml                        |  11 +-
 doc/src/sgml/protocol.sgml                    |  52 ++++++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 doc/src/sgml/system-views.sgml                |  11 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_functions.sql      |   1 +
 src/backend/catalog/system_views.sql          |   6 +-
 src/backend/commands/subscriptioncmds.c       | 105 ++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  38 +++-
 src/backend/replication/logical/tablesync.c   |   1 +
 src/backend/replication/logical/worker.c      |   7 +
 src/backend/replication/repl_gram.y           |  20 ++-
 src/backend/replication/repl_scanner.l        |   2 +
 src/backend/replication/slot.c                |  33 +++-
 src/backend/replication/slotfuncs.c           |  22 ++-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |  64 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/info.c                     |   5 +-
 src/bin/pg_upgrade/pg_upgrade.c               |   6 +-
 src/bin/pg_upgrade/pg_upgrade.h               |   2 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_proc.dat               |  14 +-
 src/include/catalog/pg_subscription.h         |  11 ++
 src/include/nodes/replnodes.h                 |  12 ++
 src/include/replication/slot.h                |   9 +-
 src/include/replication/walreceiver.h         |  18 +-
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 39 files changed, 745 insertions(+), 124 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..f224327c00 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the physical standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..154c426df7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        physical standbys so that logical replication can be resumed
+        after failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..af997efd2b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..ce386a46a7 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the physical standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..1868b95836 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the physical
+       standbys so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
@@ -1357,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..f50be29d99 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,30 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set failover for a subscription that does not have a slot name")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified if
+					 * the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1512,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error
+	 * while doing the database operations we won't be able to rollback
+	 * altered slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 201c36cb22..c5232cee2f 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -74,8 +74,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -96,6 +99,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -899,8 +903,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -929,7 +933,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -998,6 +1003,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 911835c5cb..3dea10f9b3 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..696376400e 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to physical standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -311,6 +314,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -679,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, true);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..c93dba855b 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -133,7 +135,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +174,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +192,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +431,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -782,11 +789,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 728059518e..e29a6196a3 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,13 +1217,14 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad74e07dbb..e6e4bbdfb9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,17 +11123,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ca32625585..c92a81e6b7 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,12 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -145,6 +151,11 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..585ccbb504 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for physical standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,9 +224,10 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..513c7702ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3880,6 +3881,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v64_2-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uDK=DfChioOaHon3LGZ7UusVLzzv_nYpoZVrzHX08Jdgw@mail.gmail.com/5-v64_2-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From ae7d7fd01e32fe416d2373fb2f0167b22c8cc0d1 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 16:22:05 +0530
Subject: [PATCH v64_2 5/6] Non replication connection and app_name change.

Changes in this patch:
1) Convert replication connection to non-replication one in slotsync worker.
2) Use app_name as {cluster_name}_slotsyncworker in the slotsync worker
connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index f50be29d99..13da6b1466 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1530,7 +1532,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1781,7 +1784,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 934c1a3ab0..d6f0cae0a9 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -50,7 +53,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -125,7 +129,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -136,8 +145,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	const char *keys[6];
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 9ffdf5d3ba..1e0f0ac3cd 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1062,6 +1062,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1163,13 +1164,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 3dea10f9b3..95e7324c8f 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 5e942cb4fc..68ac074274 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -434,8 +435,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v64_2-0004-Allow-logical-walsenders-to-wait-for-the-physi.patch (40.7K, ../../CAJpy0uDK=DfChioOaHon3LGZ7UusVLzzv_nYpoZVrzHX08Jdgw@mail.gmail.com/6-v64_2-0004-Allow-logical-walsenders-to-wait-for-the-physi.patch)
  download | inline diff:
From 214c2325369a5ee585ed4af14e635d008ce93d47 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:02:42 +0530
Subject: [PATCH v64_2 4/6] Allow logical walsenders to wait for the physical
 standbys

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 738 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 33f957b02f..d0509fb5a5 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2210,3 +2224,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index c81a7a8344..acf562fe93 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3527,6 +3599,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3596,8 +3669,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3069d8790e..dd27d58a66 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f81bef9e42..eef9f25c45 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..9a42b01f9a 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -45,6 +45,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index c17abfb40b..d50bbb3ae7 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v64_2-0006-Document-the-steps-to-check-if-the-standby-is-.patch (6.6K, ../../CAJpy0uDK=DfChioOaHon3LGZ7UusVLzzv_nYpoZVrzHX08Jdgw@mail.gmail.com/7-v64_2-0006-Document-the-steps-to-check-if-the-standby-is-.patch)
  download | inline diff:
From 9e30402eedf7095ddf6d7a0df0e3ce4afc373796 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v64_2 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 04:19  Amit Kapila <[email protected]>
  parent: Amit Kapila <[email protected]>
  2 siblings, 0 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-22 04:19 UTC (permalink / raw)
  To: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Fri, Jan 19, 2024 at 5:23 PM Amit Kapila <[email protected]> wrote:
>
> On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
> >
>
> I had some off-list discussions with Sawada-San, Hou-San, and Shveta
> on the topic of extending replication commands instead of using the
> current model where we fetch the required slot information via SQL
> using a database connection. I would like to summarize the discussion
> and would like to know the thoughts of others on this topic.
>
> In the current patch, we launch the slotsync worker on physical
> standby which connects to the specified database (currently we let
> users specify the required dbname in primary_conninfo) on the primary.
> It then fetches the required information for failover marked slots
> from the primary and also does some primitive checks on the upstream
> node via SQL (the additional checks are like whether the upstream node
> has a specified physical slot or whether the upstream node is a
> primary node or a standby node). To fetch the required information it
> uses a libpqwalreciever API which is mostly apt for this purpose as it
> supports SQL execution but for this patch, we don't need a replication
> connection, so we extend the libpqwalreciever connect API.
>
> Now, the concerns related to this could be that users would probably
> need to change existing mechanisms/tools to update priamry_conninfo
> and one of the alternatives proposed is to have an additional GUC like
> slot_sync_dbname. Users won't be able to drop the database this worker
> is connected to aka whatever is specified in slot_sync_dbname but as
> the user herself sets up the configuration it shouldn't be a big deal.
> Then we also discussed whether extending libpqwalreceiver's connect
> API is a good idea and whether we need to further extend it in the
> future. As far as I can see, slotsync worker's primary requirement is
> to execute SQL queries which the current API is sufficient, and don't
> see something that needs any drastic change in this API. Note that
> tablesync worker that executes SQL also uses these APIs, so we may
> need something in the future for either of those. Then finally we need
> a slotsync worker to also connect to a database to use SQL and fetch
> results.
>
> Now, let us consider if we extend the replication commands like
> READ_REPLICATION_SLOT and or introduce a new set of replication
> commands to fetch the required information then we don't need a DB
> connection with primary or a connection in slotsync worker. As per my
> current understanding, it is quite doable but I think we will slowly
> go in the direction of making replication commands something like SQL
> because today we need to extend it to fetch all slots info that have
> failover marked as true, the existence of a particular replication,
> etc. Then tomorrow, if we want to extend this work to have multiple
> slotsync workers say workers perdb then we have to extend the
> replication command to fetch per-database failover marked slots. To
> me, it sounds more like we are slowly adding SQL-like features to
> replication commands.
>
> Apart from this when we are reading per-db replication slots without
> connecting to a database, we probably need some additional protection
> mechanism so that the database won't get dropped.
>
> Considering all this it seems that for now probably extending
> replication commands can simplify a few things like mentioned above
> but using SQL's with db-connection is more extendable.
>
> Thoughts?
>

Bertrand, and others, do you have an opinion on this matter?

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 06:58  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-22 06:58 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Fri, Jan 19, 2024 at 3:55 PM shveta malik <[email protected]> wrote:
>
> On Fri, Jan 19, 2024 at 10:35 AM Masahiko Sawada <[email protected]> wrote:
> >
> >
> > Thank you for updating the patch. I have some comments:
> >
> > ---
> > +        latestWalEnd = GetWalRcvLatestWalEnd();
> > +        if (remote_slot->confirmed_lsn > latestWalEnd)
> > +        {
> > +                elog(ERROR, "exiting from slot synchronization as the
> > received slot sync"
> > +                         " LSN %X/%X for slot \"%s\" is ahead of the
> > standby position %X/%X",
> > +                         LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
> > +                         remote_slot->name,
> > +                         LSN_FORMAT_ARGS(latestWalEnd));
> > +        }
> >
> > IIUC GetWalRcvLatestWalEnd () returns walrcv->latestWalEnd, which is
> > typically the primary server's flush position and doesn't mean the LSN
> > where the walreceiver received/flushed up to.
>
> yes. I think it makes more sense to use something which actually tells
> flushed-position. I gave it a try by replacing GetWalRcvLatestWalEnd()
> with GetWalRcvFlushRecPtr() but I see a problem here. Lets say I have
> enabled the slot-sync feature in a running standby, in that case we
> are all good (flushedUpto is the same as actual flush-position
> indicated by LogstreamResult.Flush). But if I restart standby, then I
> observed that the startup process sets flushedUpto to some value 'x'
> (see [1]) while when the wal-receiver starts, it sets
> 'LogstreamResult.Flush' to another value (see [2]) which is always
> greater than 'x'. And we do not update flushedUpto with the
> 'LogstreamResult.Flush' value in walreceiver until we actually do an
> operation on primary. Performing a data change on primary sends WALs
> to standby which then hits XLogWalRcvFlush() and updates flushedUpto
> same as LogstreamResult.Flush. Until then we have a situation where
> slots received on standby are ahead of flushedUpto and thus slotsync
> worker keeps one erroring out. I am yet to find out why flushedUpto is
> set to a lower value than 'LogstreamResult.Flush' at the start of
> standby.  Or maybe am I using the wrong function
> GetWalRcvFlushRecPtr() and should be using something else instead?
>

Can we think of using GetStandbyFlushRecPtr()? We probably need to
expose this function, if this works for the required purpose.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 07:41  Bertrand Drouvot <[email protected]>
  parent: Amit Kapila <[email protected]>
  2 siblings, 1 reply; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-22 07:41 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Hi,

On Fri, Jan 19, 2024 at 05:23:53PM +0530, Amit Kapila wrote:
> On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
> >
> 
> Now, the concerns related to this could be that users would probably
> need to change existing mechanisms/tools to update priamry_conninfo

Yeah, for the ones that want the sync slot feature.

> and one of the alternatives proposed is to have an additional GUC like
> slot_sync_dbname. Users won't be able to drop the database this worker
> is connected to aka whatever is specified in slot_sync_dbname but as
> the user herself sets up the configuration it shouldn't be a big deal.

Same point of view here.

> Then we also discussed whether extending libpqwalreceiver's connect
> API is a good idea and whether we need to further extend it in the
> future. As far as I can see, slotsync worker's primary requirement is
> to execute SQL queries which the current API is sufficient, and don't
> see something that needs any drastic change in this API. Note that
> tablesync worker that executes SQL also uses these APIs, so we may
> need something in the future for either of those. Then finally we need
> a slotsync worker to also connect to a database to use SQL and fetch
> results.
>

On my side the nits concerns about using the libpqrcv_connect / walrcv_connect are:

- cosmetic: the "rcv" do not really align with the sync slot worker
- we're using a WalReceiverConn, while a PGconn should suffice. From what I can
see the "overhead" is (1 byte + 7 bytes hole + 8 bytes). I don't think that's a
big deal even if we switch to a multi sync slot worker design later on.

Those have already been discussed in [1] and I'm fine with them.

> Now, let us consider if we extend the replication commands like
> READ_REPLICATION_SLOT and or introduce a new set of replication
> commands to fetch the required information then we don't need a DB
> connection with primary or a connection in slotsync worker. As per my
> current understanding, it is quite doable but I think we will slowly
> go in the direction of making replication commands something like SQL
> because today we need to extend it to fetch all slots info that have
> failover marked as true, the existence of a particular replication,
> etc. Then tomorrow, if we want to extend this work to have multiple
> slotsync workers say workers perdb then we have to extend the
> replication command to fetch per-database failover marked slots. To
> me, it sounds more like we are slowly adding SQL-like features to
> replication commands.

Agree. Also it seems to me that extending the replication commands is more like
a one-way door change.

> Apart from this when we are reading per-db replication slots without
> connecting to a database, we probably need some additional protection
> mechanism so that the database won't get dropped.
> 
> Considering all this it seems that for now probably extending
> replication commands can simplify a few things like mentioned above
> but using SQL's with db-connection is more extendable.

I'd vote for using a SQL db-connection (like we are doing currently).
It seems more extendable and more a two-way door (as compared to extending the
replication commands): I think it still gives us the flexibility to switch to
extending the replication commands if we want to in the future.

[1]: https://www.postgresql.org/message-id/ZZe6sok7IWmhKReU%40ip-10-97-1-34.eu-west-3.compute.internal

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-22 08:49  Zhijie Hou (Fujitsu) <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-22 08:49 UTC (permalink / raw)
  To: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Monday, January 22, 2024 11:36 AM shveta malik <[email protected]> wrote:

Hi,

> On Fri, Jan 19, 2024 at 4:18 PM shveta malik <[email protected]> wrote:
> >
> > PFA v64.
> 
> V64 fails to apply to HEAD due to a recent commit. Rebased it. PFA v64_2. It has
> no new changes.

I noticed few things while analyzing the patch.

1.
sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);

The initial value for sleep_ms is 0(default value for static variable) which
will not be advanced in this expression. We should initialize sleep_ms to a positive
number.

2.
		/ Wait a bit, we don't expect to have to wait long /
		rc = WaitLatch(MyLatch,
					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);

The slotsync worker is not a bgworker anymore after 0003 patch, so a new event
is needed I think.

3.
slot->effective_catalog_xmin = xmin_horizon;

The assignment is also needed in local_slot_update() to make 
ReplicationSlotsComputeRequiredXmin work.

Best Regards,
Hou zj


^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 09:40  Amit Kapila <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-22 09:40 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: shveta malik <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Mon, Jan 22, 2024 at 1:11 PM Bertrand Drouvot
<[email protected]> wrote:
>
> Hi,
>

Thanks for sharing the feedback.

>
> > Then we also discussed whether extending libpqwalreceiver's connect
> > API is a good idea and whether we need to further extend it in the
> > future. As far as I can see, slotsync worker's primary requirement is
> > to execute SQL queries which the current API is sufficient, and don't
> > see something that needs any drastic change in this API. Note that
> > tablesync worker that executes SQL also uses these APIs, so we may
> > need something in the future for either of those. Then finally we need
> > a slotsync worker to also connect to a database to use SQL and fetch
> > results.
> >
>
> On my side the nits concerns about using the libpqrcv_connect / walrcv_connect are:
>
> - cosmetic: the "rcv" do not really align with the sync slot worker
>

But note that the same API is even used for apply worker as well. One
can think that this is a connection used to receive WAL or slot_info.

minor comments on the patch:
=======================
1.
+ /* First time slot update, the function must return true */
+ Assert(local_slot_update(remote_slot));

Isn't moving this code to Assert in update_and_persist_slot() wrong?
It will make this function call no-op in non-assert builds?

2.
+ ereport(LOG,
+ errmsg("newly locally created slot \"%s\" is sync-ready now",

I think even without 'locally' in the above LOG message, it is clear.

3.
+/*
+ * walrcv_get_dbinfo_for_failover_slots_fn
+ *
+ * Run LIST_DBID_FOR_FAILOVER_SLOTS on primary server to get the
+ * list of unique DBIDs for failover logical slots
+ */
+typedef List *(*walrcv_get_dbinfo_for_failover_slots_fn)
(WalReceiverConn *conn);

This looks like a leftover from the previous version of the patch.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 11:30  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: shveta malik @ 2024-01-22 11:30 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Mon, Jan 22, 2024 at 3:10 PM Amit Kapila <[email protected]> wrote:
>
> minor comments on the patch:
> =======================

PFA v65 addressing the comments.

Addressed comments by Peter in [1], comments by Hou-San in [2],
comments by Amit in [3] and [4]

TODO:
Analyze the issue reported by Swada-san in [5] (pt 2)
Disallow subscription creation on standby with failover=true (as we do
not support sync on cascading standbys)

[1]: https://www.postgresql.org/message-id/CAHut%2BPt5Pk_xJkb54oahR%2Bf9oawgfnmbpewvkZPgnRhoJ3gkYg%40mail...
[2]: https://www.postgresql.org/message-id/OS0PR01MB57160C7184E17C6765AAE38294752%40OS0PR01MB5716.jpnprd0...
[3]: https://www.postgresql.org/message-id/CAA4eK1JPB-zpGYTbVOP5Qp26tNQPMjDuYzNZ%2Ba9RFiN5nE1tEA%40mail.g...
[4]: https://www.postgresql.org/message-id/CAA4eK1Jhy1-bsu6vc0%3DNja7aw5-EK_%3D101pnnuM3ATqTA8%2B%3DSg%40...
[5]: https://www.postgresql.org/message-id/CAD21AoBgzONdt3o5mzbQ4MtqAE%3DWseiXUOq0LMqne-nWGjZBsA%40mail.g...

thanks
Shveta


Attachments:

  [application/octet-stream] v65-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.7K, ../../CAJpy0uAmbh_1pH-eG5bTbHXXq9w-uNV45DT-12TTh9kGj2HeCw@mail.gmail.com/2-v65-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 7092e0d8bb1ded68437cf5c3935accdc6d0f85a0 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Mon, 22 Jan 2024 15:49:49 +0530
Subject: [PATCH v65 4/6] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 738 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 33f957b02f..d0509fb5a5 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2210,3 +2224,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index f753aed345..c20c8a8d9e 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f81bef9e42..eef9f25c45 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index ab1e3997ec..575ce124e4 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v65-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uAmbh_1pH-eG5bTbHXXq9w-uNV45DT-12TTh9kGj2HeCw@mail.gmail.com/3-v65-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 28ff3751e66b55b847aece2da9498e28226e0099 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 16 Jan 2024 16:22:05 +0530
Subject: [PATCH v65 5/6] Non replication connection and app_name change.

Changes in this patch:
1) Convert replication connection to non-replication one in slotsync worker.
2) Use app_name as {cluster_name}_slotsyncworker in the slotsync worker
connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index f50be29d99..13da6b1466 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1530,7 +1532,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1781,7 +1784,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 934c1a3ab0..d6f0cae0a9 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -50,7 +53,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -125,7 +129,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -136,8 +145,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	const char *keys[6];
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index e6d378c987..31d99b0dc8 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1050,6 +1050,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1151,13 +1152,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..0d791c188c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 1df43b25c0..4acdf66e43 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -425,8 +426,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v65-0001-Enable-setting-failover-property-for-a-slot-thro.patch (100.3K, ../../CAJpy0uAmbh_1pH-eG5bTbHXXq9w-uNV45DT-12TTh9kGj2HeCw@mail.gmail.com/4-v65-0001-Enable-setting-failover-property-for-a-slot-thro.patch)
  download | inline diff:
From 579ad076bcf357f724665c0fc806e58f9c0bb894 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 4 Jan 2024 09:15:26 +0530
Subject: [PATCH v65 1/6] Enable setting failover property for a slot through
 SQL API and subscription commands

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the next patch will
address that.

In addition, a new replication command named ALTER_REPLICATION_SLOT and a
corresponding walreceiver API function named walrcv_alter_slot have been
implemented. These additions provide subscribers or users the ability to
modify the failover property of a replication slot on the publisher.

Moreover, a new subscription option called 'failover' has been added,
allowing users to set it when creating or altering a subscription. Also,
a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out       |  58 ++++++
 contrib/test_decoding/sql/slot.sql            |  13 ++
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/func.sgml                        |  11 +-
 doc/src/sgml/protocol.sgml                    |  52 ++++++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 doc/src/sgml/system-views.sgml                |  11 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_functions.sql      |   1 +
 src/backend/catalog/system_views.sql          |   6 +-
 src/backend/commands/subscriptioncmds.c       | 105 ++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  38 +++-
 src/backend/replication/logical/tablesync.c   |   1 +
 src/backend/replication/logical/worker.c      |   7 +
 src/backend/replication/repl_gram.y           |  20 ++-
 src/backend/replication/repl_scanner.l        |   2 +
 src/backend/replication/slot.c                |  33 +++-
 src/backend/replication/slotfuncs.c           |  22 ++-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |  64 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/info.c                     |   5 +-
 src/bin/pg_upgrade/pg_upgrade.c               |   6 +-
 src/bin/pg_upgrade/pg_upgrade.h               |   2 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_proc.dat               |  14 +-
 src/include/catalog/pg_subscription.h         |  11 ++
 src/include/nodes/replnodes.h                 |  12 ++
 src/include/replication/slot.h                |   9 +-
 src/include/replication/walreceiver.h         |  18 +-
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 39 files changed, 745 insertions(+), 124 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..f224327c00 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the physical standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..154c426df7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        physical standbys so that logical replication can be resumed
+        after failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..af997efd2b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..ce386a46a7 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the physical standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..1868b95836 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the physical
+       standbys so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
@@ -1357,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..f50be29d99 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,30 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set failover for a subscription that does not have a slot name")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified if
+					 * the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1512,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error
+	 * while doing the database operations we won't be able to rollback
+	 * altered slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 201c36cb22..c5232cee2f 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -74,8 +74,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -96,6 +99,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -899,8 +903,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -929,7 +933,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -998,6 +1003,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..696376400e 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to physical standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -311,6 +314,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -679,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, true);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..c93dba855b 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -133,7 +135,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +174,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +192,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +431,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -782,11 +789,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 728059518e..e29a6196a3 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,13 +1217,14 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad74e07dbb..e6e4bbdfb9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,17 +11123,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ca32625585..c92a81e6b7 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,12 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -145,6 +151,11 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..585ccbb504 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for physical standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,9 +224,10 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..513c7702ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3880,6 +3881,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v65-0002-Add-logical-slot-sync-capability-to-the-physical.patch (85.7K, ../../CAJpy0uAmbh_1pH-eG5bTbHXXq9w-uNV45DT-12TTh9kGj2HeCw@mail.gmail.com/5-v65-0002-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 2d8eac4ef82eb3592bfa1e5af6023a063190d45b Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Mon, 22 Jan 2024 08:57:53 +0530
Subject: [PATCH v65 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1179 +++++++++++++++++
 src/backend/replication/slot.c                |   32 +-
 src/backend/replication/slotfuncs.c           |   14 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   10 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 34 files changed, 1704 insertions(+), 42 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1868b95836..4f36a2f732 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index c5232cee2f..934c1a3ab0 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -34,6 +34,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -58,6 +59,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -100,6 +102,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -432,6 +435,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..55e08a6145
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1179 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/* Min and Max sleep time for slot sync worker */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+/*
+ * Sleep time in ms between slot-sync cycles.
+ * See wait_for_slot_activity() for how we adjust this
+ */
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			remote_dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	remote_dbid = get_database_oid(remote_slot->database, false);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	SpinLockAcquire(&slot->mutex);
+	namestrcpy(&slot->data.plugin, remote_slot->plugin);
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if(!local_slot_update(remote_slot))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received and flushed
+	 * before syncing slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		elog(ERROR, "exiting from slot synchronization as the received slot sync"
+			 " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+			 LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+			 remote_slot->name,
+			 LSN_FORMAT_ARGS(latestFlushPtr));
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on standby",
+						   remote_slot->name),
+					errdetail("A user-created slot with the same name as"
+							  " failover slot already exists on the standby."));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(remote_slot->database, false);
+		namestrcpy(&slot->data.plugin, remote_slot->plugin);
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info"
+					   " from the primary server: %s", res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, 7, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 8, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 9, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the"
+					   " primary server: %s", PrimarySlotName, res->err));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR, "failed to fetch primary_slot_name tuple");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("wal_level must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("%s must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of"
+					   " a parameter change"));
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down"
+					   " on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for logical replication launcher.
+ *
+ * Called on logical replication launcher exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the logical replication launcher, or when there
+	 * is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("enable_syncslot is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 696376400e..33f957b02f 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to physical standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -315,6 +316,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +682,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +711,16 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	/*
+	 * Do not allow users to alter the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot alter replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +733,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +889,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index c93dba855b..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..f753aed345 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e6e4bbdfb9..10e6a1e951 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,9 +11123,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 585ccbb504..f81bef9e42 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for physical standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..1df43b25c0 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -278,6 +278,12 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
  */
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
 
 /*
  * walrcv_server_version_fn
@@ -403,6 +409,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +435,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +494,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..ab1e3997ec 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT failover, synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t|t",
+	'logical slot has failover as true and synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 513c7702ff..d527bf918b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2585,6 +2586,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v65-0003-Slot-sync-worker-as-a-special-process.patch (37.1K, ../../CAJpy0uAmbh_1pH-eG5bTbHXXq9w-uNV45DT-12TTh9kGj2HeCw@mail.gmail.com/6-v65-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From 53a35fee1c49f3881890e79a2bcc9bb3bf251678 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Mon, 22 Jan 2024 15:39:47 +0530
Subject: [PATCH v65 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  85 +++--
 src/backend/replication/logical/slotsync.c    | 319 +++++++++++++-----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   7 +-
 14 files changed, 346 insertions(+), 180 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..bf051ced3d 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,9 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY)
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1015,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1835,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2686,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3045,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3217,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3599,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3745,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3762,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3778,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3877,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4101,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4915,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5019,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5848,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 55e08a6145..e6d378c987 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -27,6 +27,8 @@
  * It waits for a period of time before the next synchronization, with the
  * duration varying based on whether any slots were updated during the last
  * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
  *---------------------------------------------------------------------------
  */
 
@@ -40,7 +42,9 @@
 #include "commands/dbcommands.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -53,6 +57,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -107,8 +113,16 @@ bool		enable_syncslot = false;
  */
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -696,7 +710,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -711,8 +726,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -748,12 +765,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by %s is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -762,20 +782,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -783,10 +798,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("%s must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -794,45 +812,95 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("%s must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("wal_level must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("%s must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in %s.", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -848,16 +916,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -865,8 +945,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of"
 					   " a parameter change"));
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -883,7 +962,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down"
 					   " on receiving SIGINT"));
@@ -916,17 +996,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -962,20 +1041,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -990,26 +1087,69 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	BackgroundWorkerUnblockSignals();
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1028,26 +1168,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1064,7 +1207,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1095,7 +1238,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1138,42 +1281,62 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("enable_syncslot is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..aa53a57077 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..cd530d3d40 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,10 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v65-0006-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../CAJpy0uAmbh_1pH-eG5bTbHXXq9w-uNV45DT-12TTh9kGj2HeCw@mail.gmail.com/7-v65-0006-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From c0f0fe1b1a724f5c8ecdaad1f841b54f739f2563 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v65 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 11:33  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-22 11:33 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Mon, Jan 22, 2024 at 12:28 PM Amit Kapila <[email protected]> wrote:
>
> On Fri, Jan 19, 2024 at 3:55 PM shveta malik <[email protected]> wrote:
> >
> > On Fri, Jan 19, 2024 at 10:35 AM Masahiko Sawada <[email protected]> wrote:
> > >
> > >
> > > Thank you for updating the patch. I have some comments:
> > >
> > > ---
> > > +        latestWalEnd = GetWalRcvLatestWalEnd();
> > > +        if (remote_slot->confirmed_lsn > latestWalEnd)
> > > +        {
> > > +                elog(ERROR, "exiting from slot synchronization as the
> > > received slot sync"
> > > +                         " LSN %X/%X for slot \"%s\" is ahead of the
> > > standby position %X/%X",
> > > +                         LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
> > > +                         remote_slot->name,
> > > +                         LSN_FORMAT_ARGS(latestWalEnd));
> > > +        }
> > >
> > > IIUC GetWalRcvLatestWalEnd () returns walrcv->latestWalEnd, which is
> > > typically the primary server's flush position and doesn't mean the LSN
> > > where the walreceiver received/flushed up to.
> >
> > yes. I think it makes more sense to use something which actually tells
> > flushed-position. I gave it a try by replacing GetWalRcvLatestWalEnd()
> > with GetWalRcvFlushRecPtr() but I see a problem here. Lets say I have
> > enabled the slot-sync feature in a running standby, in that case we
> > are all good (flushedUpto is the same as actual flush-position
> > indicated by LogstreamResult.Flush). But if I restart standby, then I
> > observed that the startup process sets flushedUpto to some value 'x'
> > (see [1]) while when the wal-receiver starts, it sets
> > 'LogstreamResult.Flush' to another value (see [2]) which is always
> > greater than 'x'. And we do not update flushedUpto with the
> > 'LogstreamResult.Flush' value in walreceiver until we actually do an
> > operation on primary. Performing a data change on primary sends WALs
> > to standby which then hits XLogWalRcvFlush() and updates flushedUpto
> > same as LogstreamResult.Flush. Until then we have a situation where
> > slots received on standby are ahead of flushedUpto and thus slotsync
> > worker keeps one erroring out. I am yet to find out why flushedUpto is
> > set to a lower value than 'LogstreamResult.Flush' at the start of
> > standby.  Or maybe am I using the wrong function
> > GetWalRcvFlushRecPtr() and should be using something else instead?
> >
>
> Can we think of using GetStandbyFlushRecPtr()? We probably need to
> expose this function, if this works for the required purpose.

I think we can. For the records, the problem while using flushedUpto
(or GetWalRcvFlushRecPtr()) directly is that it is not set to the
latest flushed position immediately after startup.  It points to some
prior location (perhaps segment or page start) after startup until
some data  is flushed next which then updates it to the latest flushed
position, thus we can not use it directly.  GetStandbyFlushRecPtr()
OTOH takes care of it i.e. it returns correct flushed-location at any
point of time. I have changed v65 to use this one.

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 11:37  shveta malik <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-22 11:37 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Fri, Jan 19, 2024 at 11:48 AM Peter Smith <[email protected]> wrote:
>
> Here are some review comments for patch v63-0003.

Thanks Peter. I have addressed all in v65.

>
> 4b.
> It was a bit different when there were ERRORs but now they are LOGs
> somehow it seems wrong for this function to say what the *caller* will
> do. Maybe you can rewrite all the errmsg so the don't say "skipping"
> but they just say "bad configuration for slot synchronization"
>
> If valid is false then you can LOG "skipping" at the caller...

I have made this change but now in the log file we see 3 logs like
below, does it seem apt? Was the earlier one better where we get the
info in 2 lines?

[34416] LOG:  bad configuration for slot synchronization
[34416] HINT:  hot_standby_feedback must be enabled.
[34416] LOG:  skipping slot synchronization

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 11:58  Masahiko Sawada <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-22 11:58 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Dilip Kumar <[email protected]>; shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Sat, Jan 20, 2024 at 7:44 PM Amit Kapila <[email protected]> wrote:
>
> On Sat, Jan 20, 2024 at 10:52 AM Dilip Kumar <[email protected]> wrote:
> >
> > On Fri, Jan 19, 2024 at 5:24 PM Amit Kapila <[email protected]> wrote:
> > >
> > > On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
> > > >
> > >
> > > I had some off-list discussions with Sawada-San, Hou-San, and Shveta
> > > on the topic of extending replication commands instead of using the
> > > current model where we fetch the required slot information via SQL
> > > using a database connection. I would like to summarize the discussion
> > > and would like to know the thoughts of others on this topic.
> > >
> > > In the current patch, we launch the slotsync worker on physical
> > > standby which connects to the specified database (currently we let
> > > users specify the required dbname in primary_conninfo) on the primary.
> > > It then fetches the required information for failover marked slots
> > > from the primary and also does some primitive checks on the upstream
> > > node via SQL (the additional checks are like whether the upstream node
> > > has a specified physical slot or whether the upstream node is a
> > > primary node or a standby node). To fetch the required information it
> > > uses a libpqwalreciever API which is mostly apt for this purpose as it
> > > supports SQL execution but for this patch, we don't need a replication
> > > connection, so we extend the libpqwalreciever connect API.
> >
> > What sort of extension we have done to 'libpqwalreciever'? Is it
> > something like by default this supports replication connections so we
> > have done an extension to the API so that we can provide an option
> > whether to create a replication connection or a normal connection?
> >
>
> Yeah and in the future there could be more as well. The other function
> added walrcv_get_dbname_from_conninfo doesn't appear to be a problem
> either for now.
>
> > > Now, the concerns related to this could be that users would probably
> > > need to change existing mechanisms/tools to update priamry_conninfo

I'm concerned about this. In fact, a primary_conninfo value generated
by pg_basebackup does not work with enable_syncslot.

> > > and one of the alternatives proposed is to have an additional GUC like
> > > slot_sync_dbname. Users won't be able to drop the database this worker
> > > is connected to aka whatever is specified in slot_sync_dbname but as
> > > the user herself sets up the configuration it shouldn't be a big deal.
> >
> > Yeah for this purpose users may use template1 or so which they
> > generally don't plan to drop.
> >
>
> Using template1 has other problems like users won't be able to create
> a new database. See [2] (point number 2.2)
>
> >
> >  So in case the user wants to drop that
> > database user needs to turn off the slot syncing option and then it
> > can be done?
> >
>
> Right.

If the user wants to continue using slot syncing, they need to switch
the database to connect. Which requires modifying primary_conninfo and
reloading the configuration file. Which further leads to restarting
the physical replication. If they use synchronous replication, it
means the application temporarily stops during that.

>
> > > Then we also discussed whether extending libpqwalreceiver's connect
> > > API is a good idea and whether we need to further extend it in the
> > > future. As far as I can see, slotsync worker's primary requirement is
> > > to execute SQL queries which the current API is sufficient, and don't
> > > see something that needs any drastic change in this API. Note that
> > > tablesync worker that executes SQL also uses these APIs, so we may
> > > need something in the future for either of those. Then finally we need
> > > a slotsync worker to also connect to a database to use SQL and fetch
> > > results.
> >
> > While looking into the patch v64-0002 I could not exactly point out
> > what sort of extensions are there in libpqwalreceiver.c, I just saw
> > one extra API for fetching the dbname from connection info?
> >
>
> Right, the worry was that we may need it in the future.

Yes. IIUC the slotsync worker uses libpqwalreceiver to establish a
non-replication connection and to execute SQL query. But neither of
them are relevant with replication. I'm a bit concerned that when we
need to extend the slotsync feature in the future we will end up
extending libpqwalreceiver, even if the new feature is not also
relevant with replication.

>
> > > Now, let us consider if we extend the replication commands like
> > > READ_REPLICATION_SLOT and or introduce a new set of replication
> > > commands to fetch the required information then we don't need a DB
> > > connection with primary or a connection in slotsync worker. As per my
> > > current understanding, it is quite doable but I think we will slowly
> > > go in the direction of making replication commands something like SQL
> > > because today we need to extend it to fetch all slots info that have
> > > failover marked as true, the existence of a particular replication,
> > > etc. Then tomorrow, if we want to extend this work to have multiple
> > > slotsync workers say workers perdb then we have to extend the
> > > replication command to fetch per-database failover marked slots. To
> > > me, it sounds more like we are slowly adding SQL-like features to
> > > replication commands.

Right. How about filtering slots on the standby side? That is, for
example, the LIST_SLOT command returns all slots and the slotsync
worker filters out non-failover slots. Also such command could
potentially be used also in client tools like pg_basebackup,
pg_receivewal, and pg_recvlogical to list the available replication
slots to specify.

> > > Considering all this it seems that for now probably extending
> > > replication commands can simplify a few things like mentioned above
> > > but using SQL's with db-connection is more extendable.
> >

Agreed.

Having said that, considering Amit, Bertrand, and Dilip already agreed
with the current design (using SQL's with db-connection), I might be
worrying too much. So we can probably go with the current design and
improve it if we find some problems.

Regards,

--
Masahiko Sawada

Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 12:26  Amit Kapila <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-22 12:26 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Dilip Kumar <[email protected]>; shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Mon, Jan 22, 2024 at 5:28 PM Masahiko Sawada <[email protected]> wrote:
>
> On Sat, Jan 20, 2024 at 7:44 PM Amit Kapila <[email protected]> wrote:
> >
> > On Sat, Jan 20, 2024 at 10:52 AM Dilip Kumar <[email protected]> wrote:
> > >
> > > On Fri, Jan 19, 2024 at 5:24 PM Amit Kapila <[email protected]> wrote:
> > > >
> > > > On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
> > > > >
> > > >
> > > > I had some off-list discussions with Sawada-San, Hou-San, and Shveta
> > > > on the topic of extending replication commands instead of using the
> > > > current model where we fetch the required slot information via SQL
> > > > using a database connection. I would like to summarize the discussion
> > > > and would like to know the thoughts of others on this topic.
> > > >
> > > > In the current patch, we launch the slotsync worker on physical
> > > > standby which connects to the specified database (currently we let
> > > > users specify the required dbname in primary_conninfo) on the primary.
> > > > It then fetches the required information for failover marked slots
> > > > from the primary and also does some primitive checks on the upstream
> > > > node via SQL (the additional checks are like whether the upstream node
> > > > has a specified physical slot or whether the upstream node is a
> > > > primary node or a standby node). To fetch the required information it
> > > > uses a libpqwalreciever API which is mostly apt for this purpose as it
> > > > supports SQL execution but for this patch, we don't need a replication
> > > > connection, so we extend the libpqwalreciever connect API.
> > >
> > > What sort of extension we have done to 'libpqwalreciever'? Is it
> > > something like by default this supports replication connections so we
> > > have done an extension to the API so that we can provide an option
> > > whether to create a replication connection or a normal connection?
> > >
> >
> > Yeah and in the future there could be more as well. The other function
> > added walrcv_get_dbname_from_conninfo doesn't appear to be a problem
> > either for now.
> >
> > > > Now, the concerns related to this could be that users would probably
> > > > need to change existing mechanisms/tools to update priamry_conninfo
>
> I'm concerned about this. In fact, a primary_conninfo value generated
> by pg_basebackup does not work with enable_syncslot.
>

Right, but if we want can't we extend pg_basebackup to do that? It is
just that I am not sure that it is a good idea to extend pg_basebackup
in the first version.

> > > > and one of the alternatives proposed is to have an additional GUC like
> > > > slot_sync_dbname. Users won't be able to drop the database this worker
> > > > is connected to aka whatever is specified in slot_sync_dbname but as
> > > > the user herself sets up the configuration it shouldn't be a big deal.
> > >
> > > Yeah for this purpose users may use template1 or so which they
> > > generally don't plan to drop.
> > >
> >
> > Using template1 has other problems like users won't be able to create
> > a new database. See [2] (point number 2.2)
> >
> > >
> > >  So in case the user wants to drop that
> > > database user needs to turn off the slot syncing option and then it
> > > can be done?
> > >
> >
> > Right.
>
> If the user wants to continue using slot syncing, they need to switch
> the database to connect. Which requires modifying primary_conninfo and
> reloading the configuration file. Which further leads to restarting
> the physical replication. If they use synchronous replication, it
> means the application temporarily stops during that.
>

Yes, that would be an inconvenience but the point is we don't expect
this to change often.

> >
> > > > Then we also discussed whether extending libpqwalreceiver's connect
> > > > API is a good idea and whether we need to further extend it in the
> > > > future. As far as I can see, slotsync worker's primary requirement is
> > > > to execute SQL queries which the current API is sufficient, and don't
> > > > see something that needs any drastic change in this API. Note that
> > > > tablesync worker that executes SQL also uses these APIs, so we may
> > > > need something in the future for either of those. Then finally we need
> > > > a slotsync worker to also connect to a database to use SQL and fetch
> > > > results.
> > >
> > > While looking into the patch v64-0002 I could not exactly point out
> > > what sort of extensions are there in libpqwalreceiver.c, I just saw
> > > one extra API for fetching the dbname from connection info?
> > >
> >
> > Right, the worry was that we may need it in the future.
>
> Yes. IIUC the slotsync worker uses libpqwalreceiver to establish a
> non-replication connection and to execute SQL query. But neither of
> them are relevant with replication.
>

But we are already using libpqwalreceiver to execute SQL queries via
tablesync worker.

 I'm a bit concerned that when we
> need to extend the slotsync feature in the future we will end up
> extending libpqwalreceiver, even if the new feature is not also
> relevant with replication.
>
> >
> > > > Now, let us consider if we extend the replication commands like
> > > > READ_REPLICATION_SLOT and or introduce a new set of replication
> > > > commands to fetch the required information then we don't need a DB
> > > > connection with primary or a connection in slotsync worker. As per my
> > > > current understanding, it is quite doable but I think we will slowly
> > > > go in the direction of making replication commands something like SQL
> > > > because today we need to extend it to fetch all slots info that have
> > > > failover marked as true, the existence of a particular replication,
> > > > etc. Then tomorrow, if we want to extend this work to have multiple
> > > > slotsync workers say workers perdb then we have to extend the
> > > > replication command to fetch per-database failover marked slots. To
> > > > me, it sounds more like we are slowly adding SQL-like features to
> > > > replication commands.
>
> Right. How about filtering slots on the standby side? That is, for
> example, the LIST_SLOT command returns all slots and the slotsync
> worker filters out non-failover slots.
>

Yeah, we can do that but it could be unnecessary network overhead when
there are very few failover slots. And it may not be just one time, we
need to fetch slot information periodically.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-22 15:12  Masahiko Sawada <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-22 15:12 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Dilip Kumar <[email protected]>; shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Mon, Jan 22, 2024 at 9:26 PM Amit Kapila <[email protected]> wrote:
>
> On Mon, Jan 22, 2024 at 5:28 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Sat, Jan 20, 2024 at 7:44 PM Amit Kapila <[email protected]> wrote:
> > >
> > > On Sat, Jan 20, 2024 at 10:52 AM Dilip Kumar <[email protected]> wrote:
> > > >
> > > > On Fri, Jan 19, 2024 at 5:24 PM Amit Kapila <[email protected]> wrote:
> > > > >
> > > > > On Wed, Jan 17, 2024 at 4:00 PM shveta malik <[email protected]> wrote:
> > > > > >
> > > > >
> > > > > I had some off-list discussions with Sawada-San, Hou-San, and Shveta
> > > > > on the topic of extending replication commands instead of using the
> > > > > current model where we fetch the required slot information via SQL
> > > > > using a database connection. I would like to summarize the discussion
> > > > > and would like to know the thoughts of others on this topic.
> > > > >
> > > > > In the current patch, we launch the slotsync worker on physical
> > > > > standby which connects to the specified database (currently we let
> > > > > users specify the required dbname in primary_conninfo) on the primary.
> > > > > It then fetches the required information for failover marked slots
> > > > > from the primary and also does some primitive checks on the upstream
> > > > > node via SQL (the additional checks are like whether the upstream node
> > > > > has a specified physical slot or whether the upstream node is a
> > > > > primary node or a standby node). To fetch the required information it
> > > > > uses a libpqwalreciever API which is mostly apt for this purpose as it
> > > > > supports SQL execution but for this patch, we don't need a replication
> > > > > connection, so we extend the libpqwalreciever connect API.
> > > >
> > > > What sort of extension we have done to 'libpqwalreciever'? Is it
> > > > something like by default this supports replication connections so we
> > > > have done an extension to the API so that we can provide an option
> > > > whether to create a replication connection or a normal connection?
> > > >
> > >
> > > Yeah and in the future there could be more as well. The other function
> > > added walrcv_get_dbname_from_conninfo doesn't appear to be a problem
> > > either for now.
> > >
> > > > > Now, the concerns related to this could be that users would probably
> > > > > need to change existing mechanisms/tools to update priamry_conninfo
> >
> > I'm concerned about this. In fact, a primary_conninfo value generated
> > by pg_basebackup does not work with enable_syncslot.
> >
>
> Right, but if we want can't we extend pg_basebackup to do that? It is
> just that I am not sure that it is a good idea to extend pg_basebackup
> in the first version.

Okay.

>
> > > > > and one of the alternatives proposed is to have an additional GUC like
> > > > > slot_sync_dbname. Users won't be able to drop the database this worker
> > > > > is connected to aka whatever is specified in slot_sync_dbname but as
> > > > > the user herself sets up the configuration it shouldn't be a big deal.
> > > >
> > > > Yeah for this purpose users may use template1 or so which they
> > > > generally don't plan to drop.
> > > >
> > >
> > > Using template1 has other problems like users won't be able to create
> > > a new database. See [2] (point number 2.2)
> > >
> > > >
> > > >  So in case the user wants to drop that
> > > > database user needs to turn off the slot syncing option and then it
> > > > can be done?
> > > >
> > >
> > > Right.
> >
> > If the user wants to continue using slot syncing, they need to switch
> > the database to connect. Which requires modifying primary_conninfo and
> > reloading the configuration file. Which further leads to restarting
> > the physical replication. If they use synchronous replication, it
> > means the application temporarily stops during that.
> >
>
> Yes, that would be an inconvenience but the point is we don't expect
> this to change often.
>
> > >
> > > > > Then we also discussed whether extending libpqwalreceiver's connect
> > > > > API is a good idea and whether we need to further extend it in the
> > > > > future. As far as I can see, slotsync worker's primary requirement is
> > > > > to execute SQL queries which the current API is sufficient, and don't
> > > > > see something that needs any drastic change in this API. Note that
> > > > > tablesync worker that executes SQL also uses these APIs, so we may
> > > > > need something in the future for either of those. Then finally we need
> > > > > a slotsync worker to also connect to a database to use SQL and fetch
> > > > > results.
> > > >
> > > > While looking into the patch v64-0002 I could not exactly point out
> > > > what sort of extensions are there in libpqwalreceiver.c, I just saw
> > > > one extra API for fetching the dbname from connection info?
> > > >
> > >
> > > Right, the worry was that we may need it in the future.
> >
> > Yes. IIUC the slotsync worker uses libpqwalreceiver to establish a
> > non-replication connection and to execute SQL query. But neither of
> > them are relevant with replication.
> >
>
> But we are already using libpqwalreceiver to execute SQL queries via
> tablesync worker.

IIUC tablesync workers do both SQL queries and replication commands. I
think the slotsync worker is the first background process who does
only SQL queries in a non-replication command ( using
libpqwalreceiver).

>
>  I'm a bit concerned that when we
> > need to extend the slotsync feature in the future we will end up
> > extending libpqwalreceiver, even if the new feature is not also
> > relevant with replication.
> >
> > >
> > > > > Now, let us consider if we extend the replication commands like
> > > > > READ_REPLICATION_SLOT and or introduce a new set of replication
> > > > > commands to fetch the required information then we don't need a DB
> > > > > connection with primary or a connection in slotsync worker. As per my
> > > > > current understanding, it is quite doable but I think we will slowly
> > > > > go in the direction of making replication commands something like SQL
> > > > > because today we need to extend it to fetch all slots info that have
> > > > > failover marked as true, the existence of a particular replication,
> > > > > etc. Then tomorrow, if we want to extend this work to have multiple
> > > > > slotsync workers say workers perdb then we have to extend the
> > > > > replication command to fetch per-database failover marked slots. To
> > > > > me, it sounds more like we are slowly adding SQL-like features to
> > > > > replication commands.
> >
> > Right. How about filtering slots on the standby side? That is, for
> > example, the LIST_SLOT command returns all slots and the slotsync
> > worker filters out non-failover slots.
> >
>
> Yeah, we can do that but it could be unnecessary network overhead when
> there are very few failover slots. And it may not be just one time, we
> need to fetch slot information periodically.

True.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-23 04:14  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Peter Smith @ 2024-01-23 04:14 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Here are some review comments for v65-0002

======
0. General - GUCs in messages

I think it would be better for the GUC names to all be quoted. It's
not a rule (yet), but OTOH it seems to be the consensus most people
want. See [1].

This might impact the following messages:

0.1
+ ereport(ERROR,
+ errmsg("could not fetch primary_slot_name \"%s\" info from the"
+    " primary server: %s", PrimarySlotName, res->err));

SUGGESTION
errmsg("could not fetch primary server slot \"%s\" info from the
primary server: %s", ...)
errhint("Check if \"primary_slot_name\" is configured correctly.");

~~~

0.2
+ if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+ elog(ERROR, "failed to fetch primary_slot_name tuple");

SUGGESTION
elog(ERROR, "failed to fetch tuple for the primary server slot
specified by \"primary_slot_name\"");

~~~

0.3
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("exiting from slot synchronization due to bad configuration"),
+ /* translator: second %s is a GUC variable name */
+ errdetail("The primary server slot \"%s\" specified by %s is not valid.",
+   PrimarySlotName, "primary_slot_name"));

/specified by %s/specified by \"%s\"/

~~~

0.4
+ ereport(ERROR,
+ /* translator: %s is a GUC variable name */
+ errmsg("exiting from slot synchronization due to bad configuration"),
+ errhint("%s must be defined.", "primary_slot_name"));

/%s must be defined./\"%s\" must be defined./

~~~

0.5
+ ereport(ERROR,
+ /* translator: %s is a GUC variable name */
+ errmsg("exiting from slot synchronization due to bad configuration"),
+ errhint("%s must be enabled.", "hot_standby_feedback"));

/%s must be enabled./\"%s\" must be enabled./

~~~

0.6
+ ereport(ERROR,
+ /* translator: %s is a GUC variable name */
+ errmsg("exiting from slot synchronization due to bad configuration"),
+ errhint("wal_level must be >= logical."));

errhint("\"wal_level\" must be >= logical."))

~~~

0.7
+ if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+ ereport(ERROR,
+ /* translator: %s is a GUC variable name */
+ errmsg("exiting from slot synchronization due to bad configuration"),
+ errhint("%s must be defined.", "primary_conninfo"));

/%s must be defined./\"%s\" must be defined./

~~~

0.8
+ ereport(ERROR,
+
+ /*
+ * translator: 'dbname' is a specific option; %s is a GUC variable
+ * name
+ */
+ errmsg("exiting from slot synchronization due to bad configuration"),
+ errhint("'dbname' must be specified in %s.", "primary_conninfo"));

/must be specified in %s./must be specified in \"%s\"./

~~~

0.9
+ ereport(LOG,
+ errmsg("skipping slot synchronization"),
+ errdetail("enable_syncslot is disabled."));

errdetail("\"enable_syncslot\" is disabled."));

======
src/backend/replication/logical/slotsync.c

1.
+/* Min and Max sleep time for slot sync worker */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000 /* 30s */
+
+/*
+ * Sleep time in ms between slot-sync cycles.
+ * See wait_for_slot_activity() for how we adjust this
+ */
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;

These all belong together, so I think they share a combined comment like:

SUGGESTION
The sleep time (ms) between slot-sync cycles varies dynamically
(within a MIN/MAX range) according to slot activity. See
wait_for_slot_activity() for details.

~~~

2. update_and_persist_slot

+ /* First time slot update, the function must return true */
+ if(!local_slot_update(remote_slot))
+ elog(ERROR, "failed to update slot");

Missing whitespace after 'if'

~~~

3. synchronize_one_slot

+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("exiting from slot synchronization because same"
+    " name slot \"%s\" already exists on standby",
+    remote_slot->name),
+ errdetail("A user-created slot with the same name as"
+   " failover slot already exists on the standby."));


3a.
/on standby/on the standby/

~

3b.
Now the errmsg is changed, the errdetail doesn't seem so useful. Isn't
it repeating pretty much the same information as in the errmsg?

======
src/backend/replication/walsender.c

4. GetStandbyFlushRecPtr

 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *

Since it says "This should only be called when in recovery", should
there also be a check for that (e.g. RecoveryInProgress) in the added
Assert?

======
src/include/replication/walreceiver.h

5.
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
  TimeLineID *primary_tli);
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);

It looks like a blank line that previously existed has been lost.

======
[1] https://www.postgresql.org/message-id/CAHut%2BPsf3NewXbsFKY88Qn1ON1_dMD6343MuWdMiiM2Ds9a_wA%40mail.g...

Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-23 05:39  Amit Kapila <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-23 05:39 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Dilip Kumar <[email protected]>; shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Mon, Jan 22, 2024 at 8:42 PM Masahiko Sawada <[email protected]> wrote:
>
> On Mon, Jan 22, 2024 at 9:26 PM Amit Kapila <[email protected]> wrote:
> >
> > >
> > > Yes. IIUC the slotsync worker uses libpqwalreceiver to establish a
> > > non-replication connection and to execute SQL query. But neither of
> > > them are relevant with replication.
> > >
> >
> > But we are already using libpqwalreceiver to execute SQL queries via
> > tablesync worker.
>
> IIUC tablesync workers do both SQL queries and replication commands. I
> think the slotsync worker is the first background process who does
> only SQL queries in a non-replication command ( using
> libpqwalreceiver).
>

Yes, I agree but till now we didn't saw any problem with the same.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-23 09:07  Ajin Cherian <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Ajin Cherian @ 2024-01-23 09:07 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Mon, Jan 22, 2024 at 10:30 PM shveta malik <[email protected]>
wrote:
>
> On Mon, Jan 22, 2024 at 3:10 PM Amit Kapila <[email protected]>
wrote:
> >
> > minor comments on the patch:
> > =======================
>
> PFA v65 addressing the comments.
>
> Addressed comments by Peter in [1], comments by Hou-San in [2],
> comments by Amit in [3] and [4]
>
> TODO:
> Analyze the issue reported by Swada-san in [5] (pt 2)
> Disallow subscription creation on standby with failover=true (as we do
> not support sync on cascading standbys)
>
> [1]:
https://www.postgresql.org/message-id/CAHut%2BPt5Pk_xJkb54oahR%2Bf9oawgfnmbpewvkZPgnRhoJ3gkYg%40mail...
> [2]:
https://www.postgresql.org/message-id/OS0PR01MB57160C7184E17C6765AAE38294752%40OS0PR01MB5716.jpnprd0...
> [3]:
https://www.postgresql.org/message-id/CAA4eK1JPB-zpGYTbVOP5Qp26tNQPMjDuYzNZ%2Ba9RFiN5nE1tEA%40mail.g...
> [4]:
https://www.postgresql.org/message-id/CAA4eK1Jhy1-bsu6vc0%3DNja7aw5-EK_%3D101pnnuM3ATqTA8%2B%3DSg%40...
> [5]:
https://www.postgresql.org/message-id/CAD21AoBgzONdt3o5mzbQ4MtqAE%3DWseiXUOq0LMqne-nWGjZBsA%40mail.g...
>
>
I was doing some testing on this. What I noticed is that creating
subscriptions with failover enabled is taking a lot longer compared with a
subscription with failover disabled. The setup has primary configured with
standby_slot_names and that standby is enabled with enable_synclot turned
on.

Publisher has one publication, no tables.
subscriber:
postgres=# \timing
Timing is on.
postgres=# CREATE SUBSCRIPTION sub CONNECTION 'dbname=postgres
host=localhost port=6972' PUBLICATION pub with (failover = true);
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION
Time: 10011.829 ms (00:10.012)

== drop the sub

postgres=# CREATE SUBSCRIPTION sub CONNECTION 'dbname=postgres
host=localhost port=6972' PUBLICATION pub with (failover = false);
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION
Time: 46.317 ms

With failover=true, it takes 10011 ms while failover=false takes 46 ms.

I don't see a similar delay when creating slot on the primary with
pg_create_logical_replication_slot() with failover flag enabled.

Then on primary:
postgres=# SELECT 'init' FROM
pg_create_logical_replication_slot('lsub2_slot', 'pgoutput', false, false,
true);
?column?
----------
init
(1 row)

Time: 36.125 ms
postgres=# SELECT 'init' FROM
pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false,
false);
?column?
----------
init
(1 row)

Time: 53.981 ms

regards,
Ajin Cherian
Fujitsu Australia


^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-23 11:43  shveta malik <[email protected]>
  parent: Ajin Cherian <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: shveta malik @ 2024-01-23 11:43 UTC (permalink / raw)
  To: Ajin Cherian <[email protected]>; +Cc: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 23, 2024 at 2:38 PM Ajin Cherian <[email protected]> wrote:
>
> I was doing some testing on this. What I noticed is that creating subscriptions with failover enabled is taking a lot longer compared with a subscription with failover disabled. The setup has primary configured with standby_slot_names and that standby is enabled with enable_synclot turned on.
>

Thanks Ajin for testing the patch. PFA v66 which fixes this issue.

The overall changes in this version are:

patch 001
1) Restricted enabling failover for user created slots on standby.
2) Fixed a wrong NOTICE during alter-sub which was always saying that
'changed the failover state to false' even if it was switched to true.

patch 002:
3) Addressed Peter's comment in [1]

patch 003:
4) Fixed the drop-db issue reported by Swada-San in [2]
5) Added other signal-handlers.
6) Fixed CFBot Windows compilation failure.

patch 004:
7) Fixed the issue reported by Ajin above in [3]. The performance
issue was due to the additional wait in WalSndWaitForWal() for
failover slots. Create Subscription calls
DecodingContextFindStartpoint() which then reads WALs to build the
initial snapshot which ends up calling WalSndWaitForWal() which waits
for standby confirmation for the case of failover slots. Addressed it
by skipping the wait during Create Sub as it is not needed there. We
now wait only if 'replication_active' is true.

Thanks Nisha for reporting the NOTICE issue (addressed in 2) and
working on issue #6.

Thanks Hou-San for working on #7.

[1]: https://www.postgresql.org/message-id/CAHut%2BPs6p6Km8_Hfy6X0KTuyqBKkhC84u23sQnnkhqkHuDL%2BDQ%40mail...
[2]: https://www.postgresql.org/message-id/CAD21AoBgzONdt3o5mzbQ4MtqAE%3DWseiXUOq0LMqne-nWGjZBsA%40mail.g...
[3]: https://www.postgresql.org/message-id/CAFPTHDbsZ%2BpxAubb9d9BwVNt5OB3_2s77bG6nHcAgUPPhEVmMQ%40mail.g...

thanks
Shveta


Attachments:

  [application/octet-stream] v66-0003-Slot-sync-worker-as-a-special-process.patch (37.3K, ../../CAJpy0uBqS_HD30mBR0yD1SVCvUZDrw1=dwn1xH8ANEQw7gnPdw@mail.gmail.com/2-v66-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From b01c9a5d900793ca361d83ecd20bbcab13df6cbd Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:34:21 +0530
Subject: [PATCH v66 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  85 +++--
 src/backend/replication/logical/slotsync.c    | 325 +++++++++++++-----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   7 +-
 14 files changed, 352 insertions(+), 180 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..bf051ced3d 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,9 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY)
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1015,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1835,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2686,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3045,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3217,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3599,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3745,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3762,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3778,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3877,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4101,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4915,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5019,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5848,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index b4c7b414b4..ef34e92c3d 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -27,6 +27,8 @@
  * It waits for a period of time before the next synchronization, with the
  * duration varying based on whether any slots were updated during the last
  * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
  *---------------------------------------------------------------------------
  */
 
@@ -40,7 +42,9 @@
 #include "commands/dbcommands.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -53,6 +57,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -106,8 +112,16 @@ bool		enable_syncslot = false;
 
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -693,7 +707,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -708,8 +723,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -747,13 +764,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -762,20 +782,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -783,10 +798,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -794,45 +812,95 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -848,16 +916,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -865,8 +945,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of"
 					   " a parameter change"));
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -883,7 +962,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down"
 					   " on receiving SIGINT"));
@@ -916,17 +996,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -962,20 +1041,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
+
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
 
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -990,26 +1087,74 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	BackgroundWorkerUnblockSignals();
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1028,26 +1173,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1064,7 +1212,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1095,7 +1243,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1138,42 +1286,63 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..aa53a57077 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..cd530d3d40 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,10 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v66-0001-Enable-setting-failover-property-for-a-slot-thro.patch (101.8K, ../../CAJpy0uBqS_HD30mBR0yD1SVCvUZDrw1=dwn1xH8ANEQw7gnPdw@mail.gmail.com/3-v66-0001-Enable-setting-failover-property-for-a-slot-thro.patch)
  download | inline diff:
From 636c4cd2c0483242a6448d3f98f5b37ff9973645 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 4 Jan 2024 09:15:26 +0530
Subject: [PATCH v66 1/6] Enable setting failover property for a slot through
 SQL API and subscription commands

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the next patch will
address that.

In addition, a new replication command named ALTER_REPLICATION_SLOT and a
corresponding walreceiver API function named walrcv_alter_slot have been
implemented. These additions provide subscribers or users the ability to
modify the failover property of a replication slot on the publisher.

Moreover, a new subscription option called 'failover' has been added,
allowing users to set it when creating or altering a subscription. Also,
a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out       |  58 ++++++
 contrib/test_decoding/sql/slot.sql            |  13 ++
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/func.sgml                        |  11 +-
 doc/src/sgml/protocol.sgml                    |  52 ++++++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 doc/src/sgml/system-views.sgml                |  11 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_functions.sql      |   1 +
 src/backend/catalog/system_views.sql          |   6 +-
 src/backend/commands/subscriptioncmds.c       | 105 ++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  38 +++-
 src/backend/replication/logical/tablesync.c   |   1 +
 src/backend/replication/logical/worker.c      |   7 +
 src/backend/replication/repl_gram.y           |  20 ++-
 src/backend/replication/repl_scanner.l        |   2 +
 src/backend/replication/slot.c                |  43 ++++-
 src/backend/replication/slotfuncs.c           |  32 +++-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |  74 +++++++-
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/info.c                     |   5 +-
 src/bin/pg_upgrade/pg_upgrade.c               |   6 +-
 src/bin/pg_upgrade/pg_upgrade.h               |   2 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_proc.dat               |  14 +-
 src/include/catalog/pg_subscription.h         |  11 ++
 src/include/nodes/replnodes.h                 |  12 ++
 src/include/replication/slot.h                |   9 +-
 src/include/replication/walreceiver.h         |  18 +-
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 39 files changed, 775 insertions(+), 124 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..f224327c00 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the physical standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..154c426df7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        physical standbys so that logical replication can be resumed
+        after failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..af997efd2b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..ce386a46a7 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the physical standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..1868b95836 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the physical
+       standbys so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
@@ -1357,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..6e2dc5841e 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,30 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set failover for a subscription that does not have a slot name")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified if
+					 * the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1512,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error
+	 * while doing the database operations we won't be able to rollback
+	 * altered slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 77669074e8..20d5128c0e 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -73,8 +73,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -95,6 +98,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -938,8 +942,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -968,7 +972,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -1037,6 +1042,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..a0a348c48c 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to physical standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -311,6 +314,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -679,6 +683,41 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, true);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	/*
+	 * Do not allow users to alter slots to enable failover on the standby
+	 * as we do not support sync to the cascading standby.
+	 */
+	if (RecoveryInProgress() && failover)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot alter replication slot to have failover"
+					   " enabled on the standby"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..3220e87f87 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -124,6 +126,16 @@ create_logical_replication_slot(char *name, char *plugin,
 
 	Assert(!MyReplicationSlot);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 */
+	if (RecoveryInProgress() && failover)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot create replication slot with failover"
+					   " enabled on the standby"));
+
 	/*
 	 * Acquire a logical decoding slot, this will check for conflicting names.
 	 * Initially create persistent slot as ephemeral - that allows us to
@@ -133,7 +145,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +184,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +202,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +247,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +441,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -782,11 +799,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 728059518e..e29a6196a3 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..f175288d82 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,13 +1217,14 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1234,6 +1246,16 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 		CheckLogicalDecodingRequirements();
 
+		/*
+		 * Do not allow users to create the slots with failover enabled on the
+		 * standby as we do not support sync to the cascading standby.
+		 */
+		if (RecoveryInProgress() && failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot create replication slot with failover"
+						   " enabled on the standby"));
+
 		/*
 		 * Initially create persistent slot as ephemeral - that allows us to
 		 * nicely handle errors during initialization because it'll get
@@ -1243,7 +1265,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1420,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2030,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad74e07dbb..e6e4bbdfb9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,17 +11123,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..956170183a 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,12 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -148,6 +154,11 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the physical
+								 * standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..585ccbb504 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for physical standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,9 +224,10 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..513c7702ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3880,6 +3881,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v66-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uBqS_HD30mBR0yD1SVCvUZDrw1=dwn1xH8ANEQw7gnPdw@mail.gmail.com/4-v66-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From bb831e9343bf9e8abc62583c37eca6434dcf14ed Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v66 5/6] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 6e2dc5841e..cb867ea2a6 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1530,7 +1532,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1781,7 +1784,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index ae76c098b1..13a787b8bf 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index ef34e92c3d..1d85e5ee62 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1050,6 +1050,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1156,13 +1157,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..0d791c188c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v66-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.7K, ../../CAJpy0uBqS_HD30mBR0yD1SVCvUZDrw1=dwn1xH8ANEQw7gnPdw@mail.gmail.com/5-v66-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From e715d3382a6b6f6eeba991e37f0b8ba71fe215da Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Mon, 22 Jan 2024 15:49:49 +0530
Subject: [PATCH v66 4/6] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 738 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index f8d83a3c6c..ddea3a6b2c 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2223,3 +2237,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index c73a83c8b7..6b0e6d1a16 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -484,6 +485,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -524,6 +527,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 65f4c36a48..ebd02a65f8 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1738,27 +1737,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1779,7 +1829,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1794,8 +1844,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1823,9 +1883,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1865,9 +1934,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2275,6 +2346,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3542,6 +3614,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3611,8 +3684,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f81bef9e42..eef9f25c45 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index ab1e3997ec..575ce124e4 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v66-0002-Add-logical-slot-sync-capability-to-the-physical.patch (86.4K, ../../CAJpy0uBqS_HD30mBR0yD1SVCvUZDrw1=dwn1xH8ANEQw7gnPdw@mail.gmail.com/6-v66-0002-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 7e38b0438abfd677a7e26b7bc4f2331e4bc363aa Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:03:11 +0530
Subject: [PATCH v66 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1179 +++++++++++++++++
 src/backend/replication/slot.c                |   53 +-
 src/backend/replication/slotfuncs.c           |   14 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 34 files changed, 1717 insertions(+), 51 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1868b95836..4f36a2f732 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 20d5128c0e..ae76c098b1 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..b4c7b414b4
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1179 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/* The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			remote_dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	remote_dbid = get_database_oid(remote_slot->database, false);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	SpinLockAcquire(&slot->mutex);
+	namestrcpy(&slot->data.plugin, remote_slot->plugin);
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received and flushed
+	 * before syncing slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		elog(ERROR, "exiting from slot synchronization as the received slot sync"
+			 " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+			 LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+			 remote_slot->name,
+			 LSN_FORMAT_ARGS(latestFlushPtr));
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = get_database_oid(remote_slot->database, false);
+		namestrcpy(&slot->data.plugin, remote_slot->plugin);
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info"
+					   " from the primary server: %s", res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, 7, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 8, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 9, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the"
+					   " primary server: %s", PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR, "failed to fetch tuple for the primary server slot"
+			 " specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of"
+					   " a parameter change"));
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down"
+					   " on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for logical replication launcher.
+ *
+ * Called on logical replication launcher exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the logical replication launcher, or when there
+	 * is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index a0a348c48c..f8d83a3c6c 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to physical standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -315,6 +316,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +682,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,15 +711,28 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
-	/*
-	 * Do not allow users to alter slots to enable failover on the standby
-	 * as we do not support sync to the cascading standby.
-	 */
-	if (RecoveryInProgress() && failover)
-		ereport(ERROR,
-				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				errmsg("cannot alter replication slot to have failover"
-					   " enabled on the standby"));
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter the slots to enable failover on the
+		 * standby as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot alter replication slot to have failover"
+						   " enabled on the standby"));
+	}
 
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
@@ -721,7 +746,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -877,8 +902,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 3220e87f87..c73a83c8b7 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -146,7 +146,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -247,7 +247,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -428,21 +428,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index f175288d82..65f4c36a48 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1265,7 +1265,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3385,14 +3385,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3401,6 +3404,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e6e4bbdfb9..10e6a1e951 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,9 +11123,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 585ccbb504..f81bef9e42 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for physical standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..ab1e3997ec 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT failover, synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t|t",
+	'logical slot has failover as true and synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 513c7702ff..d527bf918b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2585,6 +2586,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v66-0006-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../CAJpy0uBqS_HD30mBR0yD1SVCvUZDrw1=dwn1xH8ANEQw7gnPdw@mail.gmail.com/7-v66-0006-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From a67851c32e34ba5a17c90ffcd95a483abe88658e Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v66 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-23 11:50  shveta malik <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-23 11:50 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 23, 2024 at 9:45 AM Peter Smith <[email protected]> wrote:
>
> Here are some review comments for v65-0002

Thanks Peter for the feedback. I have addressed these in v66.

>
> 4. GetStandbyFlushRecPtr
>
>  /*
> - * Returns the latest point in WAL that has been safely flushed to disk, and
> - * can be sent to the standby. This should only be called when in recovery,
> - * ie. we're streaming to a cascaded standby.
> + * Returns the latest point in WAL that has been safely flushed to disk.
> + * This should only be called when in recovery.
> + *
>
> Since it says "This should only be called when in recovery", should
> there also be a check for that (e.g. RecoveryInProgress) in the added
> Assert?

Since 'am_cascading_walsender' and  'IsLogicalSlotSyncWorker' makes
sense 'in-recovery' only, I think explicit check for
'RecoveryInProgress' is not needed here. But I can add if others also
think it is needed.

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 03:21  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Peter Smith @ 2024-01-24 03:21 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Ajin Cherian <[email protected]>; Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Here are some comments for patch v66-0001.

======
doc/src/sgml/catalogs.sgml

1.
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the physical standbys
+      </para></entry>

/physical standbys/physical standby/

I wondered if it is better just to say singular "standby" instead of
"standbys" in places like this; e.g. plural might imply cascading for
some readers.

There are a number of examples like this, so I've repeated the same
comment multiple times below. If you disagree, please just ignore all
of them.

======
doc/src/sgml/func.sgml

2.
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ...
LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        physical standbys so that logical replication can be resumed
+        after failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>

(same as above)

/physical standbys/physical standby/

Also, I don't see anything else on this page using plural "standbys".

======
doc/src/sgml/protocol.sgml

3. CREATE_REPLICATION_SLOT

+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable
class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>

(same as above)

/physical standbys/physical standby/

~~~

4. ALTER_REPLICATION_SLOT

+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable
class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the physical
+          standbys so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>

(same as above)

/physical standbys/physical standby/

======
doc/src/sgml/ref/create_subscription.sgml

5.
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the
subscription
+          are enabled to be synced to the physical standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>

(same as above)

/physical standbys/physical standby/

======
doc/src/sgml/system-views.sgml

6.
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the physical
+       standbys so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>

(same as above)

/physical standbys/physical standby/

======
src/backend/commands/subscriptioncmds.c

7.
+ if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+ {
+ if (!sub->slotname)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot set failover for a subscription that does not have a
slot name")));
+
+ /*
+ * Do not allow changing the failover state if the
+ * subscription is enabled. This is because the failover
+ * state of the slot on the publisher cannot be modified if
+ * the slot is currently acquired by the apply worker.
+ */
+ if (sub->enabled)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot set %s for enabled subscription",
+ "failover")));
+
+ values[Anum_pg_subscription_subfailover - 1] =
+ BoolGetDatum(opts.failover);
+ replaces[Anum_pg_subscription_subfailover - 1] = true;
+ }

The first message is not consistent with the second. The "failover"
option maybe should be extracted so it won't be translated.

SUGGESTION
errmsg("cannot set %s for a subscription that does not have a slot
name", "failover")

~~~

8. AlterSubscription

+ if (!wrconn)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not connect to the publisher: %s", err)));
+

Need to keep an eye on the patch proposed by Nisha [1] for messages
similar to this one, so in case that gets pushed this code should be
changed appropriately.

======
src/backend/replication/slot.c

9.
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to physical standbys so
+ *     that logical replication can be resumed after failover.
  */

(same as earlier)

/physical standbys/physical standby/

~~~

10.
+ /*
+ * Do not allow users to alter slots to enable failover on the standby
+ * as we do not support sync to the cascading standby.
+ */
+ if (RecoveryInProgress() && failover)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter replication slot to have failover"
+    " enabled on the standby"));

I felt the errmsg could be expressed with less ambiguity:

SUGGESTION:
cannot enable failover for a replication slot on the standby

======
src/backend/replication/slotfuncs.c

11. create_physical_replication_slot

  /* acquire replication slot, this will check for conflicting names */
  ReplicationSlotCreate(name, false,
-   temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+   temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+   false);

Having an inline comment might be helpful here instead of passing "false,false"

SUGGESTION
ReplicationSlotCreate(name, false,
 temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
 false /* failover */);

~~~

12. create_logical_replication_slot

+ /*
+ * Do not allow users to create the slots with failover enabled on the
+ * standby as we do not support sync to the cascading standby.
+ */
+ if (RecoveryInProgress() && failover)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot create replication slot with failover"
+    " enabled on the standby"));

(similar to previous comment)

SUGGESTION:
cannot enable failover for a replication slot created on the standby

~~~

13. copy_replication_slot

  * hence pass find_startpoint false.  confirmed_flush will be set
  * below, by copying from the source slot.
+ *
+ * To avoid potential issues with the slotsync worker when the
+ * restart_lsn of a replication slot goes backwards, we set the
+ * failover option to false here. This situation occurs when a slot on
+ * the primary server is dropped and immediately replaced with a new
+ * slot of the same name, created by copying from another existing
+ * slot. However, the slotsync worker will only observe the restart_lsn
+ * of the same slot going backwards.
  */
  create_logical_replication_slot(NameStr(*dst_name),
  plugin,
  temporary,
  false,
+ false,
  src_restart_lsn,
  false);
(similar to an earlier comment)

Having an inline comment might be helpful here.

e.g. false /* failover */,

======
src/backend/replication/walreceiver.c

14.
- walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+ walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);

(similar to an earlier comment)

Having an inline comment might be helpful here:

SUGGESTION
walrcv_create_slot(wrconn, slotname, true, false, false /* failover
*/, 0, NULL);

======
src/backend/replication/walsender.c

15. CreateReplicationSlot

  ReplicationSlotCreate(cmd->slotname, false,
    cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-   false);
+   false, false);

(similar to an earlier comment)

Having an inline comment might be helpful here.

e.g. false /* failover */,

~~~

16. CreateReplicationSlot

+ /*
+ * Do not allow users to create the slots with failover enabled on the
+ * standby as we do not support sync to the cascading standby.
+ */
+ if (RecoveryInProgress() && failover)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot create replication slot with failover"
+    " enabled on the standby"));
+
  /*
  * Initially create persistent slot as ephemeral - that allows us to
  * nicely handle errors during initialization because it'll get
@@ -1243,7 +1265,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
  */
  ReplicationSlotCreate(cmd->slotname, true,
    cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-   two_phase);
+   two_phase, failover);

This errmsg seems to be repeated in a few places, so I wondered if
this code can be refactored to call direct to
create_logical_replication_slot() so the errmsg can be just once in a
common place.

OTOH, if it cannot be refactored, then needs to be using same errmsg
as suggested by earlier review comments (see above).

======
src/include/catalog/pg_subscription.h

17.
+ bool subfailover; /* True if the associated replication slots
+ * (i.e. the main slot and the table sync
+ * slots) in the upstream database are enabled
+ * to be synchronized to the physical
+ * standbys. */

(same as earlier)

/physical standbys/physical standby/

~~~

18.
+ bool failover; /* True if the associated replication slots
+ * (i.e. the main slot and the table sync
+ * slots) in the upstream database are enabled
+ * to be synchronized to the physical
+ * standbys. */

(same as earlier)

/physical standbys/physical standby/

======
src/include/replication/slot.h

19.
+
+ /*
+ * Is this a failover slot (sync candidate for physical standbys)? Only
+ * relevant for logical slots on the primary server.
+ */
+ bool failover;

(same as earlier)

/physical standbys/physical standby/

======
[1] Nisha errmsg -
https://www.postgresql.org/message-id/CABdArM5-VR4Akt_AHap_0Ofne0cTcsdnN6FcNe%2BMU8eXsa_ERQ%40mail.g...

Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 04:09  Amit Kapila <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-24 04:09 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Wed, Jan 24, 2024 at 8:52 AM Peter Smith <[email protected]> wrote:
>
> Here are some comments for patch v66-0001.
>
> ======
> doc/src/sgml/catalogs.sgml
>
> 1.
> +      <para>
> +       If true, the associated replication slots (i.e. the main slot and the
> +       table sync slots) in the upstream database are enabled to be
> +       synchronized to the physical standbys
> +      </para></entry>
>
> /physical standbys/physical standby/
>
> I wondered if it is better just to say singular "standby" instead of
> "standbys" in places like this; e.g. plural might imply cascading for
> some readers.
>

I don't think it is confusing as we used in a similar way in docs. We
can probably avoid using physical in places similar to above as that
is implied

>
>
> ======
> src/backend/replication/slotfuncs.c
>
> 11. create_physical_replication_slot
>
>   /* acquire replication slot, this will check for conflicting names */
>   ReplicationSlotCreate(name, false,
> -   temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
> +   temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
> +   false);
>
> Having an inline comment might be helpful here instead of passing "false,false"
>
> SUGGESTION
> ReplicationSlotCreate(name, false,
>  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
>  false /* failover */);
>

I don't think we follow to use of inline comments. I feel that
sometimes makes code difficult to read considering when we have
multiple such parameters.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 05:10  Masahiko Sawada <[email protected]>
  parent: Amit Kapila <[email protected]>
  1 sibling, 2 replies; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-24 05:10 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Mon, Jan 22, 2024 at 3:58 PM Amit Kapila <[email protected]> wrote:
>
> On Fri, Jan 19, 2024 at 3:55 PM shveta malik <[email protected]> wrote:
> >
> > On Fri, Jan 19, 2024 at 10:35 AM Masahiko Sawada <[email protected]> wrote:
> > >
> > >
> > > Thank you for updating the patch. I have some comments:
> > >
> > > ---
> > > +        latestWalEnd = GetWalRcvLatestWalEnd();
> > > +        if (remote_slot->confirmed_lsn > latestWalEnd)
> > > +        {
> > > +                elog(ERROR, "exiting from slot synchronization as the
> > > received slot sync"
> > > +                         " LSN %X/%X for slot \"%s\" is ahead of the
> > > standby position %X/%X",
> > > +                         LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
> > > +                         remote_slot->name,
> > > +                         LSN_FORMAT_ARGS(latestWalEnd));
> > > +        }
> > >
> > > IIUC GetWalRcvLatestWalEnd () returns walrcv->latestWalEnd, which is
> > > typically the primary server's flush position and doesn't mean the LSN
> > > where the walreceiver received/flushed up to.
> >
> > yes. I think it makes more sense to use something which actually tells
> > flushed-position. I gave it a try by replacing GetWalRcvLatestWalEnd()
> > with GetWalRcvFlushRecPtr() but I see a problem here. Lets say I have
> > enabled the slot-sync feature in a running standby, in that case we
> > are all good (flushedUpto is the same as actual flush-position
> > indicated by LogstreamResult.Flush). But if I restart standby, then I
> > observed that the startup process sets flushedUpto to some value 'x'
> > (see [1]) while when the wal-receiver starts, it sets
> > 'LogstreamResult.Flush' to another value (see [2]) which is always
> > greater than 'x'. And we do not update flushedUpto with the
> > 'LogstreamResult.Flush' value in walreceiver until we actually do an
> > operation on primary. Performing a data change on primary sends WALs
> > to standby which then hits XLogWalRcvFlush() and updates flushedUpto
> > same as LogstreamResult.Flush. Until then we have a situation where
> > slots received on standby are ahead of flushedUpto and thus slotsync
> > worker keeps one erroring out. I am yet to find out why flushedUpto is
> > set to a lower value than 'LogstreamResult.Flush' at the start of
> > standby.  Or maybe am I using the wrong function
> > GetWalRcvFlushRecPtr() and should be using something else instead?
> >
>
> Can we think of using GetStandbyFlushRecPtr()? We probably need to
> expose this function, if this works for the required purpose.

GetStandbyFlushRecPtr() seems good. But do we really want to raise an
ERROR in this case? IIUC this case could happen often when the slot
used by the standby is not listed in standby_slot_names. I think we
can just skip such a slot to synchronize and check it the next time.

Here are random comments on slotsyncworker.c (v66):

---
The postmaster relaunches the slotsync worker without intervals. So if
a connection string in primary_conninfo is not correct, many errors
are emitted.

---
+/* GUC variable */
+bool       enable_syncslot = false;

Is enable_syncslot a really good name? We use "enable" prefix only for
planner parameters such as enable_seqscan, and it seems to me that
"slot" is not specific. Other candidates are:

* synchronize_replication_slots = on|off
* synchronize_failover_slots = on|off

---
+               elog(ERROR,
+                    "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+                    " to remote slot's LSN(%X/%X) as synchronization"
+                    " would move it backwards", remote_slot->name,

Many error messages in slotsync.c are splitted into several lines, but
I think it would reduce the greppability when the user looks for the
error message in the source code.

---
+       SpinLockAcquire(&slot->mutex);
+       slot->data.database = get_database_oid(remote_slot->database, false);
+       namestrcpy(&slot->data.plugin, remote_slot->plugin);

We should not access syscaches while holding a spinlock.

---
+       SpinLockAcquire(&slot->mutex);
+       slot->data.database = get_database_oid(remote_slot->database, false);
+       namestrcpy(&slot->data.plugin, remote_slot->plugin);
+       SpinLockRelease(&slot->mutex);

Similarly, it's better to avoid calling namestrcpy() while holding a
spinlock, as we do in CreateInitDecodingContext().

---
+   SpinLockAcquire(&SlotSyncWorker->mutex);
+
+   SlotSyncWorker->stopSignaled = true;
+
+   if (SlotSyncWorker->pid == InvalidPid)
+   {
+       SpinLockRelease(&SlotSyncWorker->mutex);
+       return;
+   }
+
+   kill(SlotSyncWorker->pid, SIGINT);
+
+   SpinLockRelease(&SlotSyncWorker->mutex);

It's better to avoid calling a system call while holding a spin lock.

---
+   BackgroundWorkerUnblockSignals();

I think it's no longer necessary.

---
+       ereport(LOG,
+       /* translator: %s is a GUC variable name */
+               errmsg("bad configuration for slot synchronization"),
+               errhint("\"wal_level\" must be >= logical."));

There is no '%s' in errmsg string.

---
+/*
+ * Cleanup function for logical replication launcher.
+ *
+ * Called on logical replication launcher exit.
+ */

IIUC this function is never called by logical replication launcher.

---
+   /*
+    * The slot sync worker can not get here because it will only stop when it
+    * receives a SIGINT from the logical replication launcher, or when there
+    * is an error.
+    */
+   Assert(false);

This comment is not correct. IIUC the slotsync worker receives a
SIGINT from the startup process.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 05:43  Amit Kapila <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-24 05:43 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 24, 2024 at 10:41 AM Masahiko Sawada <[email protected]> wrote:
>
> On Mon, Jan 22, 2024 at 3:58 PM Amit Kapila <[email protected]> wrote:
> >
> > Can we think of using GetStandbyFlushRecPtr()? We probably need to
> > expose this function, if this works for the required purpose.
>
> GetStandbyFlushRecPtr() seems good. But do we really want to raise an
> ERROR in this case? IIUC this case could happen often when the slot
> used by the standby is not listed in standby_slot_names.
>

or it can be due to some bug in the code as well.

> I think we
> can just skip such a slot to synchronize and check it the next time.
>

How about logging the message and then skipping the sync step? This
will at least make users aware that they could be missing to set
standby_slot_names.

> Here are random comments on slotsyncworker.c (v66):
>
> +/* GUC variable */
> +bool       enable_syncslot = false;
>
> Is enable_syncslot a really good name? We use "enable" prefix only for
> planner parameters such as enable_seqscan, and it seems to me that
> "slot" is not specific. Other candidates are:
>
> * synchronize_replication_slots = on|off
> * synchronize_failover_slots = on|off
>

I would prefer the second one. Would it be better to just say
sync_failover_slots?

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 05:53  Masahiko Sawada <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-24 05:53 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 24, 2024 at 2:43 PM Amit Kapila <[email protected]> wrote:
>
> On Wed, Jan 24, 2024 at 10:41 AM Masahiko Sawada <[email protected]> wrote:
> >
> > On Mon, Jan 22, 2024 at 3:58 PM Amit Kapila <[email protected]> wrote:
> > >
> > > Can we think of using GetStandbyFlushRecPtr()? We probably need to
> > > expose this function, if this works for the required purpose.
> >
> > GetStandbyFlushRecPtr() seems good. But do we really want to raise an
> > ERROR in this case? IIUC this case could happen often when the slot
> > used by the standby is not listed in standby_slot_names.
> >
>
> or it can be due to some bug in the code as well.
>
> > I think we
> > can just skip such a slot to synchronize and check it the next time.
> >
>
> How about logging the message and then skipping the sync step? This
> will at least make users aware that they could be missing to set
> standby_slot_names.

+1

>
> > Here are random comments on slotsyncworker.c (v66):
> >
> > +/* GUC variable */
> > +bool       enable_syncslot = false;
> >
> > Is enable_syncslot a really good name? We use "enable" prefix only for
> > planner parameters such as enable_seqscan, and it seems to me that
> > "slot" is not specific. Other candidates are:
> >
> > * synchronize_replication_slots = on|off
> > * synchronize_failover_slots = on|off
> >
>
> I would prefer the second one. Would it be better to just say
> sync_failover_slots?

Works for me. But if we want to extend this option for non-failover
slots as well in the future, synchronize_replication_slots (or
sync_replication_slots) seems better. We can extend it by having an
enum later. For example, the values can be on, off, or failover etc.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 08:21  Amit Kapila <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-24 08:21 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 24, 2024 at 11:24 AM Masahiko Sawada <[email protected]> wrote:
>
> On Wed, Jan 24, 2024 at 2:43 PM Amit Kapila <[email protected]> wrote:
> >
> > >
> > > +/* GUC variable */
> > > +bool       enable_syncslot = false;
> > >
> > > Is enable_syncslot a really good name? We use "enable" prefix only for
> > > planner parameters such as enable_seqscan, and it seems to me that
> > > "slot" is not specific. Other candidates are:
> > >
> > > * synchronize_replication_slots = on|off
> > > * synchronize_failover_slots = on|off
> > >
> >
> > I would prefer the second one. Would it be better to just say
> > sync_failover_slots?
>
> Works for me. But if we want to extend this option for non-failover
> slots as well in the future, synchronize_replication_slots (or
> sync_replication_slots) seems better. We can extend it by having an
> enum later. For example, the values can be on, off, or failover etc.
>

I see your point. Let us see if others have any suggestions on this.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 09:08  Bertrand Drouvot <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-24 09:08 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Hi,

On Wed, Jan 24, 2024 at 01:51:54PM +0530, Amit Kapila wrote:
> On Wed, Jan 24, 2024 at 11:24 AM Masahiko Sawada <[email protected]> wrote:
> >
> > On Wed, Jan 24, 2024 at 2:43 PM Amit Kapila <[email protected]> wrote:
> > >
> > > >
> > > > +/* GUC variable */
> > > > +bool       enable_syncslot = false;
> > > >
> > > > Is enable_syncslot a really good name? We use "enable" prefix only for
> > > > planner parameters such as enable_seqscan, and it seems to me that
> > > > "slot" is not specific. Other candidates are:
> > > >
> > > > * synchronize_replication_slots = on|off
> > > > * synchronize_failover_slots = on|off
> > > >
> > >
> > > I would prefer the second one. Would it be better to just say
> > > sync_failover_slots?
> >
> > Works for me. But if we want to extend this option for non-failover
> > slots as well in the future, synchronize_replication_slots (or
> > sync_replication_slots) seems better. We can extend it by having an
> > enum later. For example, the values can be on, off, or failover etc.
> >
> 
> I see your point. Let us see if others have any suggestions on this.

I also see Sawada-San's point and I'd vote for "sync_replication_slots". Then for
the current feature I think "failover" and "on" should be the values to turn the
feature on (assuming "on" would mean "all kind of supported slots").

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 10:30  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-24 10:30 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Ajin Cherian <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Tue, Jan 23, 2024 at 5:13 PM shveta malik <[email protected]> wrote:
>
> Thanks Ajin for testing the patch. PFA v66 which fixes this issue.
>

I think we should try to commit the patch as all of the design
concerns are resolved now. To achieve that, can we split the failover
setting patch into the following: (a) setting failover property via
SQL commands and display it in pg_replication_slots (b) replication
protocol command (c) failover property via subscription commands?

It will make each patch smaller and it would be easier to detect any
problem in the same after commit.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 10:39  shveta malik <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: shveta malik @ 2024-01-24 10:39 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Wed, Jan 24, 2024 at 2:38 PM Bertrand Drouvot
<[email protected]> wrote:
>
> Hi,
>
> On Wed, Jan 24, 2024 at 01:51:54PM +0530, Amit Kapila wrote:
> > On Wed, Jan 24, 2024 at 11:24 AM Masahiko Sawada <[email protected]> wrote:
> > >
> > > On Wed, Jan 24, 2024 at 2:43 PM Amit Kapila <[email protected]> wrote:
> > > >
> > > > >
> > > > > +/* GUC variable */
> > > > > +bool       enable_syncslot = false;
> > > > >
> > > > > Is enable_syncslot a really good name? We use "enable" prefix only for
> > > > > planner parameters such as enable_seqscan, and it seems to me that
> > > > > "slot" is not specific. Other candidates are:
> > > > >
> > > > > * synchronize_replication_slots = on|off
> > > > > * synchronize_failover_slots = on|off
> > > > >
> > > >
> > > > I would prefer the second one. Would it be better to just say
> > > > sync_failover_slots?
> > >
> > > Works for me. But if we want to extend this option for non-failover
> > > slots as well in the future, synchronize_replication_slots (or
> > > sync_replication_slots) seems better. We can extend it by having an
> > > enum later. For example, the values can be on, off, or failover etc.
> > >
> >
> > I see your point. Let us see if others have any suggestions on this.
>
> I also see Sawada-San's point and I'd vote for "sync_replication_slots". Then for
> the current feature I think "failover" and "on" should be the values to turn the
> feature on (assuming "on" would mean "all kind of supported slots").

Even if others agree and we change this GUC name to
"sync_replication_slots", I feel we should keep the values as "on" and
"off" currently, where "on" would mean 'sync failover slots' (docs can
state that clearly).  I do not think we should support sync of "all
kinds of supported slots" in the first version. Maybe we can think
about it for future versions.

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-24 11:47  shveta malik <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 3 replies; 119+ messages in thread

From: shveta malik @ 2024-01-24 11:47 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Wed, Jan 24, 2024 at 4:09 PM shveta malik <[email protected]> wrote:
>
> Even if others agree and we change this GUC name to
> "sync_replication_slots", I feel we should keep the values as "on" and
> "off" currently, where "on" would mean 'sync failover slots' (docs can
> state that clearly).  I do not think we should support sync of "all
> kinds of supported slots" in the first version. Maybe we can think
> about it for future versions.

PFA v67. Note that the GUC (enable_syncslot) name is unchanged. Once
we have final agreement on the name, we can make the change in the
next version.

Changes in v67 are:

1) Addressed comments by Peter given in [1].
2) Addressed comments by Swada-San given in [2].
3) Removed syncing 'failover' on standby from remote_slot. The
'failover' field will be false for synced slots. Since we do not
support sync to cascading standbys yet, thus failover=true was
misleading and unused there.

Thanks Hou-San for contributing in 2.

Changes are split across patch001,002 and 003.

TODO:
--Split patch-001 as suggested in [3].
--Change GUC name.

[1]: https://www.postgresql.org/message-id/CAHut%2BPu_uK%3D%3DM%2BVmCMug7m7O6LAwpC05A%3DT7zP8c4G2-hS%2Bbd...
[2]: https://www.postgresql.org/message-id/CAD21AoApGoTZu7D_7%3DbVYQqKnj%2BPZ2Rz%2Bnc8Ky1HPQMS_XL6%2BA%40...
[3]: https://www.postgresql.org/message-id/CAA4eK1Lxvfq9RwOEsguiMCrKPUc1He9UGz1_wi0N0cJaXFa4Eg%40mail.gma...


thanks
Shveta


Attachments:

  [application/octet-stream] v67-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uALV+-dHZohApDPpcEXCdNhF9PFbT5BNr4WLGXZ-qHaOg@mail.gmail.com/2-v67-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From bd017528754a625a9070045874e11990599fcc17 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v67 5/6] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 15bb10da54..f17c666ebc 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1531,7 +1533,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1782,7 +1785,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index ae76c098b1..13a787b8bf 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 2a033647b3..39d66b3ed0 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1067,6 +1067,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1176,13 +1177,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..0d791c188c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v67-0001-Enable-setting-failover-property-for-a-slot-thro.patch (101.1K, ../../CAJpy0uALV+-dHZohApDPpcEXCdNhF9PFbT5BNr4WLGXZ-qHaOg@mail.gmail.com/3-v67-0001-Enable-setting-failover-property-for-a-slot-thro.patch)
  download | inline diff:
From 42e706ff63a6ad5db35c003d44da3baba9486ec7 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 4 Jan 2024 09:15:26 +0530
Subject: [PATCH v67 1/6] Enable setting failover property for a slot through
 SQL API and subscription commands

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the next patch will
address that.

In addition, a new replication command named ALTER_REPLICATION_SLOT and a
corresponding walreceiver API function named walrcv_alter_slot have been
implemented. These additions provide subscribers or users the ability to
modify the failover property of a replication slot on the publisher.

Moreover, a new subscription option called 'failover' has been added,
allowing users to set it when creating or altering a subscription. Also,
a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out       |  58 ++++++
 contrib/test_decoding/sql/slot.sql            |  13 ++
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/func.sgml                        |  11 +-
 doc/src/sgml/protocol.sgml                    |  52 ++++++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 doc/src/sgml/system-views.sgml                |  11 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_functions.sql      |   1 +
 src/backend/catalog/system_views.sql          |   6 +-
 src/backend/commands/subscriptioncmds.c       | 106 ++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  38 +++-
 src/backend/replication/logical/tablesync.c   |   1 +
 src/backend/replication/logical/worker.c      |   7 +
 src/backend/replication/repl_gram.y           |  20 ++-
 src/backend/replication/repl_scanner.l        |   2 +
 src/backend/replication/slot.c                |  53 +++++-
 src/backend/replication/slotfuncs.c           |  22 ++-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |  64 ++++++-
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/info.c                     |   5 +-
 src/bin/pg_upgrade/pg_upgrade.c               |   6 +-
 src/bin/pg_upgrade/pg_upgrade.h               |   2 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_proc.dat               |  14 +-
 src/include/catalog/pg_subscription.h         |   9 +
 src/include/nodes/replnodes.h                 |  12 ++
 src/include/replication/slot.h                |   9 +-
 src/include/replication/walreceiver.h         |  18 +-
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/rules.out           |   5 +-
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 39 files changed, 764 insertions(+), 124 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..49d8cc6826 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..0ee76835bc 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        standbys so that logical replication can be resumed after
+        failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..dc10b1956a 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..59a9554bf0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..88fe4b6341 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the standbys
+       so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
@@ -1357,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..15bb10da54 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1513,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 77669074e8..20d5128c0e 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -73,8 +73,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -95,6 +98,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -938,8 +942,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -968,7 +972,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -1037,6 +1042,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..e72bf7c06d 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -260,6 +263,16 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 
 	ReplicationSlotValidateName(name, ERROR);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 */
+	if (RecoveryInProgress() && failover)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " created on the standby"));
+
 	/*
 	 * If some other backend ran this code concurrently with us, we'd likely
 	 * both allocate the same slot, and that would be bad.  We'd also be at
@@ -311,6 +324,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -679,6 +693,41 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, true);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	/*
+	 * Do not allow users to alter slots to enable failover on the standby as
+	 * we do not support sync to the cascading standby.
+	 */
+	if (RecoveryInProgress() && failover)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " on the standby"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..c93dba855b 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -133,7 +135,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +174,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +192,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +431,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -782,11 +789,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 728059518e..e29a6196a3 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,13 +1217,14 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad74e07dbb..e6e4bbdfb9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,17 +11123,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..4d0e364624 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -148,6 +153,10 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..72ef4859ee 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,9 +224,10 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..513c7702ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3880,6 +3881,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v67-0002-Add-logical-slot-sync-capability-to-the-physical.patch (86.6K, ../../CAJpy0uALV+-dHZohApDPpcEXCdNhF9PFbT5BNr4WLGXZ-qHaOg@mail.gmail.com/4-v67-0002-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From c8b0c5ccb9efb6b14cca8ba5a578b88fe1c4fce9 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Wed, 24 Jan 2024 10:09:40 +0530
Subject: [PATCH v67 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1195 +++++++++++++++++
 src/backend/replication/slot.c                |   53 +-
 src/backend/replication/slotfuncs.c           |   14 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 34 files changed, 1733 insertions(+), 51 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 88fe4b6341..459101af5f 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 20d5128c0e..ae76c098b1 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..84b66104f7
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1195 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/* The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			remote_dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+	NameData	plugin_name;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	remote_dbid = get_database_oid(remote_slot->database, false);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	/*
+	 * We don't want any complicated code while holding a spinlock, so do
+	 * namestrcpy() outside.
+	 */
+	namestrcpy(&plugin_name, remote_slot->plugin);
+
+	SpinLockAcquire(&slot->mutex);
+	slot->data.plugin = plugin_name;
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received and flushed
+	 * before syncing slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization as the received slot sync"
+					   " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+					   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+					   remote_slot->name,
+					   LSN_FORMAT_ARGS(latestFlushPtr)));
+
+		return false;
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		Oid			dbid;
+		NameData	plugin_name;
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		/*
+		 * It is not needed to sync 'failover' from remote_slot as currently
+		 * slot synchronization is not supported on the cascading standby.
+		 */
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  false /* failover */ ,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		/*
+		 * We don't want any complicated code while holding a spinlock, so do
+		 * namestrcpy() and get_database_oid() outside.
+		 */
+		namestrcpy(&plugin_name, remote_slot->plugin);
+		dbid = get_database_oid(remote_slot->database, false);
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = dbid;
+		slot->data.plugin = plugin_name;
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 8
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info from the primary server: %s",
+					   res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 7, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 8, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the primary server: %s",
+					   PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR,
+			 "failed to fetch tuple for the primary server slot specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of a parameter change"));
+
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for slotsync worker.
+ *
+ * Called on slotsync worker exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the startup process, or when there is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index e72bf7c06d..59a1606f90 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -325,6 +326,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -690,6 +692,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -709,15 +721,28 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
-	/*
-	 * Do not allow users to alter slots to enable failover on the standby as
-	 * we do not support sync to the cascading standby.
-	 */
-	if (RecoveryInProgress() && failover)
-		ereport(ERROR,
-				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				errmsg("cannot enable failover for a replication slot"
-					   " on the standby"));
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter slots to enable failover on the standby
+		 * as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot enable failover for a replication slot"
+						   " on the standby"));
+	}
 
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
@@ -731,7 +756,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -887,8 +912,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index c93dba855b..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..f753aed345 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e6e4bbdfb9..10e6a1e951 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,9 +11123,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 72ef4859ee..41f0cc35f3 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..1055573cde 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t",
+	'logical slot has synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 513c7702ff..d527bf918b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2585,6 +2586,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v67-0003-Slot-sync-worker-as-a-special-process.patch (38.3K, ../../CAJpy0uALV+-dHZohApDPpcEXCdNhF9PFbT5BNr4WLGXZ-qHaOg@mail.gmail.com/5-v67-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From 6e551b073be73965122728466da7834383604c20 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Wed, 24 Jan 2024 16:19:04 +0530
Subject: [PATCH v67 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  86 +++--
 src/backend/replication/logical/slotsync.c    | 360 ++++++++++++++----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   8 +-
 14 files changed, 387 insertions(+), 182 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..adfaf75cf9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,10 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY && \
+	 SlotSyncWorkerCanRestart())
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1016,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1836,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2687,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3046,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3218,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3600,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3746,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3763,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3779,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3878,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4102,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4916,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5020,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5849,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 84b66104f7..2a033647b3 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -40,9 +40,12 @@
 #include "access/xlogrecovery.h"
 #include "catalog/pg_database.h"
 #include "commands/dbcommands.h"
+#include "libpq/pqsignal.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -55,6 +58,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -98,7 +103,8 @@ SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
 /* GUC variable */
 bool		enable_syncslot = false;
 
-/* The sleep time (ms) between slot-sync cycles varies dynamically
+/*
+ * The sleep time (ms) between slot-sync cycles varies dynamically
  * (within a MIN/MAX range) according to slot activity. See
  * wait_for_slot_activity() for details.
  */
@@ -107,8 +113,16 @@ bool		enable_syncslot = false;
 
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -712,7 +726,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -727,8 +742,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -766,13 +783,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -781,20 +801,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -802,10 +817,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -813,45 +831,94 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
-		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+	{
+		ereport(LOG,
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -867,16 +934,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -884,8 +963,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of a parameter change"));
 
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -902,7 +980,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
 		proc_exit(0);
@@ -934,17 +1013,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -980,20 +1058,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -1008,26 +1104,77 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	/*
+	 * Unblock signals (they were blocked when the postmaster forked us)
+	 */
+	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1046,26 +1193,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1081,7 +1231,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1111,7 +1261,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1154,42 +1304,92 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * SlotSyncWorkerCanRestart
+ *
+ * Returns true if the worker is allowed to restart if enough time has
+ * passed (SLOTSYNC_RESTART_INTERVAL_SEC) since it was launched last.
+ * Otherwise returns false.
+ *
+ * This is a safety valve to protect against continuous respawn attempts if the
+ * worker is dying immediately at launch. Note that since we will retry to
+ * launch the worker from the postmaster main loop, we will get another
+ * chance later.
+ */
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+	static time_t last_slotsync_start_time = 0;
+	time_t		curtime = time(NULL);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+	/* Return false if too soon since last start. */
+	if ((unsigned int) (curtime - last_slotsync_start_time) <
+		(unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC)
+		return false;
+
+	last_slotsync_start_time = curtime;
+	return true;
+}
+
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
+
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
+
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..aa53a57077 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..aa01f63648 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,11 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
+extern bool SlotSyncWorkerCanRestart(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v67-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.7K, ../../CAJpy0uALV+-dHZohApDPpcEXCdNhF9PFbT5BNr4WLGXZ-qHaOg@mail.gmail.com/6-v67-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 71068f5daa826f19c719ab234988a194bd70d786 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Mon, 22 Jan 2024 15:49:49 +0530
Subject: [PATCH v67 4/6] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 738 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 59a1606f90..37599ceb4c 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2233,3 +2247,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index f753aed345..71933f4bf1 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 41f0cc35f3..a0bad469fd 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 1055573cde..06a4ada941 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v67-0006-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../CAJpy0uALV+-dHZohApDPpcEXCdNhF9PFbT5BNr4WLGXZ-qHaOg@mail.gmail.com/7-v67-0006-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From a5d4470b2340965314d632c033064c72eca17d19 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v67 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 01:37  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  2 siblings, 0 replies; 119+ messages in thread

From: Peter Smith @ 2024-01-25 01:37 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Here are some review comments for the patch v67-0001.

======
1.
There are a couple of places checking for failover usage on a standby.

+ if (RecoveryInProgress() && failover)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot enable failover for a replication slot"
+    " created on the standby"));

and

+ if (RecoveryInProgress() && failover)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot enable failover for a replication slot"
+    " on the standby"));

IMO the conditions should be written the other way around (failover &&
RecoveryInProgress()) to avoid the unnecessary function calls when
'failover' flag is probably mostly default false anyway.

======
Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-25 02:57  Zhijie Hou (Fujitsu) <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-25 02:57 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; shveta malik <[email protected]>; +Cc: Ajin Cherian <[email protected]>; Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Wednesday, January 24, 2024 6:31 PM Amit Kapila <[email protected]> wrote:
> 
> On Tue, Jan 23, 2024 at 5:13 PM shveta malik <[email protected]> wrote:
> >
> > Thanks Ajin for testing the patch. PFA v66 which fixes this issue.
> >
> 
> I think we should try to commit the patch as all of the design concerns are
> resolved now. To achieve that, can we split the failover setting patch into the
> following: (a) setting failover property via SQL commands and display it in
> pg_replication_slots (b) replication protocol command (c) failover property via
> subscription commands?
> 
> It will make each patch smaller and it would be easier to detect any problem in
> the same after commit.

Agreed. I split the original 0001 patch into 3 patches as suggested.
Here is the V68 patch set.

Best Regards,
Hou zj


Attachments:

  [application/octet-stream] v68-0005-Slot-sync-worker-as-a-special-process.patch (38.3K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/2-v68-0005-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From a0c5734905f01c52ca288fcc146cb7d425c9d52f Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Wed, 24 Jan 2024 16:19:04 +0530
Subject: [PATCH v68 5/8] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  86 +++--
 src/backend/replication/logical/slotsync.c    | 360 ++++++++++++++----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   8 +-
 14 files changed, 387 insertions(+), 182 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..adfaf75cf9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,10 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY && \
+	 SlotSyncWorkerCanRestart())
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1016,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1836,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2687,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3046,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3218,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3600,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3746,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3763,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3779,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3878,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4102,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4916,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5020,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5849,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 84b66104f7..2a033647b3 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -40,9 +40,12 @@
 #include "access/xlogrecovery.h"
 #include "catalog/pg_database.h"
 #include "commands/dbcommands.h"
+#include "libpq/pqsignal.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -55,6 +58,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -98,7 +103,8 @@ SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
 /* GUC variable */
 bool		enable_syncslot = false;
 
-/* The sleep time (ms) between slot-sync cycles varies dynamically
+/*
+ * The sleep time (ms) between slot-sync cycles varies dynamically
  * (within a MIN/MAX range) according to slot activity. See
  * wait_for_slot_activity() for details.
  */
@@ -107,8 +113,16 @@ bool		enable_syncslot = false;
 
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -712,7 +726,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -727,8 +742,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -766,13 +783,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -781,20 +801,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -802,10 +817,13 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -813,45 +831,94 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
-		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+	{
+		ereport(LOG,
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -867,16 +934,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -884,8 +963,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of a parameter change"));
 
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -902,7 +980,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
 		proc_exit(0);
@@ -934,17 +1013,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -980,20 +1058,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -1008,26 +1104,77 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	/*
+	 * Unblock signals (they were blocked when the postmaster forked us)
+	 */
+	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1046,26 +1193,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1081,7 +1231,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1111,7 +1261,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1154,42 +1304,92 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * SlotSyncWorkerCanRestart
+ *
+ * Returns true if the worker is allowed to restart if enough time has
+ * passed (SLOTSYNC_RESTART_INTERVAL_SEC) since it was launched last.
+ * Otherwise returns false.
+ *
+ * This is a safety valve to protect against continuous respawn attempts if the
+ * worker is dying immediately at launch. Note that since we will retry to
+ * launch the worker from the postmaster main loop, we will get another
+ * chance later.
+ */
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+	static time_t last_slotsync_start_time = 0;
+	time_t		curtime = time(NULL);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+	/* Return false if too soon since last start. */
+	if ((unsigned int) (curtime - last_slotsync_start_time) <
+		(unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC)
+		return false;
+
+	last_slotsync_start_time = curtime;
+	return true;
+}
+
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
+
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
+
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..aa53a57077 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..aa01f63648 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,11 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
+extern bool SlotSyncWorkerCanRestart(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.30.0.windows.2



  [application/octet-stream] v68-0006-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.7K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/3-v68-0006-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 3255cfcd01df176f9673ab33fa62e76e799d1cad Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Mon, 22 Jan 2024 15:49:49 +0530
Subject: [PATCH v68 6/8] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 16 files changed, 738 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index f07147a68a..a7c1eb84aa 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,13 +46,18 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -99,10 +104,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2233,3 +2247,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index f753aed345..71933f4bf1 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 41f0cc35f3..a0bad469fd 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 1055573cde..06a4ada941 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.30.0.windows.2



  [application/octet-stream] v68-0007-Non-replication-connection-and-app_name-change.patch (9.7K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/4-v68-0007-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 093630966d073c3c141e275cd52e9f55afbc7bfb Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v68 7/8] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 15bb10da54..f17c666ebc 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1531,7 +1533,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1782,7 +1785,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index ae76c098b1..13a787b8bf 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 2a033647b3..39d66b3ed0 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1067,6 +1067,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1176,13 +1177,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..0d791c188c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.30.0.windows.2



  [application/octet-stream] v68-0008-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/5-v68-0008-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From 7670b16d5c3a6f9c22284c4f90be3089308345c8 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v68 8/8] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.30.0.windows.2



  [application/octet-stream] v68-0001-Add-the-failover-property-to-replication-slot.patch (20.4K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/6-v68-0001-Add-the-failover-property-to-replication-slot.patch)
  download | inline diff:
From 60a8101d03fe5780402b563f166ace619ddad374 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:14:52 +0800
Subject: [PATCH v68 1/8] Add the failover property to replication slot.

This commit adds the failover property to the replication slot. The
failover property indicates whether the slot will be synced to the standby
servers, enabling the resumption of corresponding logical replication
after failover. But note that this commit does not yet include the
capability to actually sync the replication slot; the subsequent commit will
address that.

In addition, a new parameter 'failover' is added to the
pg_create_logical_replication_slot function.

The value of the 'failover' flag is displayed as part of
pg_replication_slots view.
---
 contrib/test_decoding/expected/slot.out  | 58 ++++++++++++++++++++++++
 contrib/test_decoding/sql/slot.sql       | 13 ++++++
 doc/src/sgml/func.sgml                   | 11 +++--
 doc/src/sgml/system-views.sgml           | 11 +++++
 src/backend/catalog/system_functions.sql |  1 +
 src/backend/catalog/system_views.sql     |  3 +-
 src/backend/replication/slot.c           |  8 +++-
 src/backend/replication/slotfuncs.c      | 16 +++++--
 src/backend/replication/walsender.c      |  4 +-
 src/bin/pg_upgrade/info.c                |  5 +-
 src/bin/pg_upgrade/pg_upgrade.c          |  6 ++-
 src/bin/pg_upgrade/pg_upgrade.h          |  2 +
 src/include/catalog/pg_proc.dat          | 14 +++---
 src/include/replication/slot.h           |  8 +++-
 src/test/regress/expected/rules.out      |  5 +-
 15 files changed, 141 insertions(+), 24 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 63a9940f73..261d8886d3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -406,3 +406,61 @@ SELECT pg_drop_replication_slot('copied_slot2_notemp');
  
 (1 row)
 
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+       slot_name       | slot_type | failover 
+-----------------------+-----------+----------
+ failover_true_slot    | logical   | t
+ failover_false_slot   | logical   | f
+ failover_default_slot | logical   | f
+ physical_slot         | physical  | f
+(4 rows)
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_false_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('failover_default_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 1aa27c5667..45aeae7fd5 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -176,3 +176,16 @@ ORDER BY o.slot_name, c.slot_name;
 SELECT pg_drop_replication_slot('orig_slot2');
 SELECT pg_drop_replication_slot('copied_slot2_no_change');
 SELECT pg_drop_replication_slot('copied_slot2_notemp');
+
+-- Test failover option of slots.
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_true_slot', 'test_decoding', false, false, true);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_false_slot', 'test_decoding', false, false, false);
+SELECT 'init' FROM pg_create_logical_replication_slot('failover_default_slot', 'test_decoding', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot');
+
+SELECT slot_name, slot_type, failover FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('failover_true_slot');
+SELECT pg_drop_replication_slot('failover_false_slot');
+SELECT pg_drop_replication_slot('failover_default_slot');
+SELECT pg_drop_replication_slot('physical_slot');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..0ee76835bc 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27655,7 +27655,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <indexterm>
          <primary>pg_create_logical_replication_slot</primary>
         </indexterm>
-        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type> </optional> )
+        <function>pg_create_logical_replication_slot</function> ( <parameter>slot_name</parameter> <type>name</type>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type>, <parameter>twophase</parameter> <type>boolean</type>, <parameter>failover</parameter> <type>boolean</type> </optional> )
         <returnvalue>record</returnvalue>
         ( <parameter>slot_name</parameter> <type>name</type>,
         <parameter>lsn</parameter> <type>pg_lsn</type> )
@@ -27670,8 +27670,13 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         released upon any error. The optional fourth parameter,
         <parameter>twophase</parameter>, when set to true, specifies
         that the decoding of prepared transactions is enabled for this
-        slot. A call to this function has the same effect as the replication
-        protocol command <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
+        slot. The optional fifth parameter,
+        <parameter>failover</parameter>, when set to true,
+        specifies that this slot is enabled to be synced to the
+        standbys so that logical replication can be resumed after
+        failover. A call to this function has the same effect as
+        the replication protocol command
+        <literal>CREATE_REPLICATION_SLOT ... LOGICAL</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 72d01fc624..88fe4b6341 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2555,6 +2555,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>failover</structfield> <type>bool</type>
+      </para>
+      <para>
+       True if this is a logical slot enabled to be synced to the standbys
+       so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index f315fecf18..346cfb98a0 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -479,6 +479,7 @@ CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot(
     IN slot_name name, IN plugin name,
     IN temporary boolean DEFAULT false,
     IN twophase boolean DEFAULT false,
+    IN failover boolean DEFAULT false,
     OUT slot_name name, OUT lsn pg_lsn)
 RETURNS RECORD
 LANGUAGE INTERNAL
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43e36f5ac..d0f711a619 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1023,7 +1023,8 @@ CREATE VIEW pg_replication_slots AS
             L.wal_status,
             L.safe_wal_size,
             L.two_phase,
-            L.conflict_reason
+            L.conflict_reason,
+            L.failover
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 52da694c79..02a14ec210 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -90,7 +90,7 @@ typedef struct ReplicationSlotOnDisk
 	sizeof(ReplicationSlotOnDisk) - ReplicationSlotOnDiskConstantSize
 
 #define SLOT_MAGIC		0x1051CA1	/* format identifier */
-#define SLOT_VERSION	3		/* version for new files */
+#define SLOT_VERSION	4		/* version for new files */
 
 /* Control array for replication slot management */
 ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
@@ -248,10 +248,13 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     during getting changes, if the two_phase option is enabled it can skip
  *     prepare because by that time start decoding point has been moved. So the
  *     user will only get commit prepared.
+ * failover: If enabled, allows the slot to be synced to standbys so
+ *     that logical replication can be resumed after failover.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
-					  ReplicationSlotPersistency persistency, bool two_phase)
+					  ReplicationSlotPersistency persistency,
+					  bool two_phase, bool failover)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -311,6 +314,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.persistency = persistency;
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
+	slot->data.failover = failover;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index cad35dce7f..eb685089b3 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -42,7 +42,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false);
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
+						  false);
 
 	if (immediately_reserve)
 	{
@@ -117,6 +118,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 static void
 create_logical_replication_slot(char *name, char *plugin,
 								bool temporary, bool two_phase,
+								bool failover,
 								XLogRecPtr restart_lsn,
 								bool find_startpoint)
 {
@@ -133,7 +135,8 @@ create_logical_replication_slot(char *name, char *plugin,
 	 * error as well.
 	 */
 	ReplicationSlotCreate(name, true,
-						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase);
+						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
+						  failover);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -171,6 +174,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	Name		plugin = PG_GETARG_NAME(1);
 	bool		temporary = PG_GETARG_BOOL(2);
 	bool		two_phase = PG_GETARG_BOOL(3);
+	bool		failover = PG_GETARG_BOOL(4);
 	Datum		result;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
@@ -188,6 +192,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 									NameStr(*plugin),
 									temporary,
 									two_phase,
+									failover,
 									InvalidXLogRecPtr,
 									true);
 
@@ -232,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 15
+#define PG_GET_REPLICATION_SLOTS_COLS 16
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -426,6 +431,8 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 			}
 		}
 
+		values[i++] = BoolGetDatum(slot_contents.data.failover);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -693,6 +700,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	XLogRecPtr	src_restart_lsn;
 	bool		src_islogical;
 	bool		temporary;
+	bool		failover;
 	char	   *plugin;
 	Datum		values[2];
 	bool		nulls[2];
@@ -748,6 +756,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	src_islogical = SlotIsLogical(&first_slot_contents);
 	src_restart_lsn = first_slot_contents.data.restart_lsn;
 	temporary = (first_slot_contents.data.persistency == RS_TEMPORARY);
+	failover = first_slot_contents.data.failover;
 	plugin = logical_slot ? NameStr(first_slot_contents.data.plugin) : NULL;
 
 	/* Check type of replication slot */
@@ -787,6 +796,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 										plugin,
 										temporary,
 										false,
+										failover,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 087031e9dc..aa80f3de20 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1212,7 +1212,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false);
+							  false, false);
 
 		if (reserve_wal)
 		{
@@ -1243,7 +1243,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase);
+							  two_phase, false);
 
 		/*
 		 * Do options check early so that we can bail before calling the
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 74e02b3f82..183c2f84eb 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -666,7 +666,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, "
+	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
 							"%s as caught_up, conflict_reason IS NOT NULL as invalid "
 							"FROM pg_catalog.pg_replication_slots "
 							"WHERE slot_type = 'logical' AND "
@@ -684,6 +684,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		int			i_slotname;
 		int			i_plugin;
 		int			i_twophase;
+		int			i_failover;
 		int			i_caught_up;
 		int			i_invalid;
 
@@ -692,6 +693,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 		i_slotname = PQfnumber(res, "slot_name");
 		i_plugin = PQfnumber(res, "plugin");
 		i_twophase = PQfnumber(res, "two_phase");
+		i_failover = PQfnumber(res, "failover");
 		i_caught_up = PQfnumber(res, "caught_up");
 		i_invalid = PQfnumber(res, "invalid");
 
@@ -702,6 +704,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 			curr->slotname = pg_strdup(PQgetvalue(res, slotnum, i_slotname));
 			curr->plugin = pg_strdup(PQgetvalue(res, slotnum, i_plugin));
 			curr->two_phase = (strcmp(PQgetvalue(res, slotnum, i_twophase), "t") == 0);
+			curr->failover = (strcmp(PQgetvalue(res, slotnum, i_failover), "t") == 0);
 			curr->caught_up = (strcmp(PQgetvalue(res, slotnum, i_caught_up), "t") == 0);
 			curr->invalid = (strcmp(PQgetvalue(res, slotnum, i_invalid), "t") == 0);
 		}
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 14a36f0503..10c94a6c1f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -916,8 +916,10 @@ create_logical_replication_slots(void)
 			appendStringLiteralConn(query, slot_info->slotname, conn);
 			appendPQExpBuffer(query, ", ");
 			appendStringLiteralConn(query, slot_info->plugin, conn);
-			appendPQExpBuffer(query, ", false, %s);",
-							  slot_info->two_phase ? "true" : "false");
+
+			appendPQExpBuffer(query, ", false, %s, %s);",
+							  slot_info->two_phase ? "true" : "false",
+							  slot_info->failover ? "true" : "false");
 
 			PQclear(executeQueryOrDie(conn, "%s", query->data));
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index a1d08c3dab..d9a848cbfd 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -160,6 +160,8 @@ typedef struct
 	bool		two_phase;		/* can the slot decode 2PC? */
 	bool		caught_up;		/* has the slot caught up to latest changes? */
 	bool		invalid;		/* if true, the slot is unusable */
+	bool		failover;		/* is the slot designated to be synced to the
+								 * physical standby? */
 } LogicalSlotInfo;
 
 typedef struct
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad74e07dbb..e6e4bbdfb9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,17 +11123,17 @@
   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,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,conflict_reason}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool}',
+  proargmodes => '{o,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,conflict_reason,failover}',
   prosrc => 'pg_get_replication_slots' },
 { oid => '3786', descr => 'set up a logical replication slot',
   proname => 'pg_create_logical_replication_slot', provolatile => 'v',
   proparallel => 'u', prorettype => 'record',
-  proargtypes => 'name name bool bool',
-  proallargtypes => '{name,name,bool,bool,name,pg_lsn}',
-  proargmodes => '{i,i,i,i,o,o}',
-  proargnames => '{slot_name,plugin,temporary,twophase,slot_name,lsn}',
+  proargtypes => 'name name bool bool bool',
+  proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,i,o,o}',
+  proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
 { oid => '4222',
   descr => 'copy a logical replication slot, changing temporality and plugin',
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9e39aaf303..f2501be553 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -111,6 +111,12 @@ typedef struct ReplicationSlotPersistentData
 
 	/* plugin name */
 	NameData	plugin;
+
+	/*
+	 * Is this a failover slot (sync candidate for standbys)? Only
+	 * relevant for logical slots on the primary server.
+	 */
+	bool		failover;
 } ReplicationSlotPersistentData;
 
 /*
@@ -218,7 +224,7 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase);
+								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
 
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 55f2e95352..57884d3fec 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1473,8 +1473,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.wal_status,
     l.safe_wal_size,
     l.two_phase,
-    l.conflict_reason
-   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, conflict_reason)
+    l.conflict_reason,
+    l.failover
+   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, conflict_reason, failover)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
-- 
2.30.0.windows.2



  [application/octet-stream] v68-0002-Allow-setting-failover-property-in-the-replicati.patch (19.7K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/7-v68-0002-Allow-setting-failover-property-in-the-replicati.patch)
  download | inline diff:
From 090be3ecaea83c59c919ef56b0fa587c4d19527c Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:04:18 +0800
Subject: [PATCH v68 2/8] Allow setting failover property in the replication
 command.

This commit implements a new replication command called ALTER_REPLICATION_SLOT
and a corresponding walreceiver API function named walrcv_alter_slot.
Additionally, the CREATE_REPLICATION_SLOT command has been extended to support
the failover option.

These new additions allow the modification of the failover property of a
replication slot on the publisher. A subsequent commit will make use of these
commands in subscription commands.
---
 doc/src/sgml/protocol.sgml                    | 52 ++++++++++++++++
 src/backend/commands/subscriptioncmds.c       |  2 +-
 .../libpqwalreceiver/libpqwalreceiver.c       | 38 +++++++++++-
 src/backend/replication/logical/tablesync.c   |  1 +
 src/backend/replication/repl_gram.y           | 20 +++++-
 src/backend/replication/repl_scanner.l        |  2 +
 src/backend/replication/slot.c                | 25 ++++++++
 src/backend/replication/walreceiver.c         |  2 +-
 src/backend/replication/walsender.c           | 62 ++++++++++++++++++-
 src/include/nodes/replnodes.h                 | 12 ++++
 src/include/replication/slot.h                |  1 +
 src/include/replication/walreceiver.h         | 18 +++++-
 src/tools/pgindent/typedefs.list              |  2 +
 13 files changed, 225 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..dc10b1956a 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,17 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2135,47 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following options are supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..eaf2ec3b36 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -807,7 +807,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 77669074e8..20d5128c0e 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -73,8 +73,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -95,6 +98,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -938,8 +942,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -968,7 +972,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -1037,6 +1042,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..4207b9356c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   false,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 02a14ec210..f2781d0455 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -683,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, false);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 728059518e..e29a6196a3 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index aa80f3de20..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,7 +1217,8 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, false);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f2501be553..72ef4859ee 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -227,6 +227,7 @@ extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..513c7702ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3880,6 +3881,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.30.0.windows.2



  [application/octet-stream] v68-0003-Add-a-failover-option-to-subscriptions.patch (63.7K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/8-v68-0003-Add-a-failover-option-to-subscriptions.patch)
  download | inline diff:
From d339f14d96ff2b1155d38e6cf0fa2de4173a55d0 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:51:39 +0800
Subject: [PATCH v68 3/8] Add a failover option to subscriptions.

This commit introduces a new subscription option named 'failover', which
provides users with the ability to set the failover property of the replication
slot on the publisher when creating or altering a subscription.
---
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/ref/alter_subscription.sgml      |  16 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_views.sql          |   3 +-
 src/backend/commands/subscriptioncmds.c       | 106 ++++++++++-
 src/backend/replication/logical/tablesync.c   |   2 +-
 src/backend/replication/logical/worker.c      |   7 +
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_subscription.h         |   9 +
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 17 files changed, 375 insertions(+), 91 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e82..49d8cc6826 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7990,6 +7990,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..b3e779df70 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,22 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..59a9554bf0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index d0f711a619..e43a93739d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1358,7 +1358,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eaf2ec3b36..15bb10da54 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1513,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4207b9356c..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,7 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
-					   false,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce..339f20e5e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d2157..24e1643794 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -665,6 +665,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f9516320..6d9ad5d74e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6563,7 +6563,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6627,6 +6628,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..4d0e364624 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -148,6 +153,10 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
-- 
2.30.0.windows.2



  [application/octet-stream] v68-0004-Add-logical-slot-sync-capability-to-the-physical.patch (88.6K, ../../OS0PR01MB5716E304C26E6ACE0BE84925947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/9-v68-0004-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From c7fcae7ff2b6aa48937991e4a26d2bd5cafe1f6d Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Thu, 25 Jan 2024 10:27:56 +0800
Subject: [PATCH v68 4/8] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/system-views.sgml                |   16 +
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1195 +++++++++++++++++
 src/backend/replication/slot.c                |   55 +-
 src/backend/replication/slotfuncs.c           |   26 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 34 files changed, 1753 insertions(+), 45 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 88fe4b6341..459101af5f 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2566,6 +2566,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 1b48d7171a..e38a9587e2 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1441,6 +1442,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e43a93739d..ad57bade07 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 20d5128c0e..ae76c098b1 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..84b66104f7
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1195 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/* The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot)
+{
+	Oid			remote_dbid;
+	ReplicationSlot *slot = MyReplicationSlot;
+	NameData	plugin_name;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	remote_dbid = get_database_oid(remote_slot->database, false);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	/*
+	 * We don't want any complicated code while holding a spinlock, so do
+	 * namestrcpy() outside.
+	 */
+	namestrcpy(&plugin_name, remote_slot->plugin);
+
+	SpinLockAcquire(&slot->mutex);
+	slot->data.plugin = plugin_name;
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(WalReceiverConn *wrconn, RemoteSlot *remote_slot)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Sanity check: Make sure that concerned WAL is received and flushed
+	 * before syncing slot to target lsn received from the primary server.
+	 *
+	 * This check should never pass as on the primary server, we have waited
+	 * for the standby's confirmation before updating the logical slot.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization as the received slot sync"
+					   " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+					   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+					   remote_slot->name,
+					   LSN_FORMAT_ARGS(latestFlushPtr)));
+
+		return false;
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		Oid			dbid;
+		NameData	plugin_name;
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		/* Ensure that we have transaction env needed by get_database_oid() */
+		Assert(IsTransactionState());
+
+		/*
+		 * It is not needed to sync 'failover' from remote_slot as currently
+		 * slot synchronization is not supported on the cascading standby.
+		 */
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  false /* failover */ ,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		/*
+		 * We don't want any complicated code while holding a spinlock, so do
+		 * namestrcpy() and get_database_oid() outside.
+		 */
+		namestrcpy(&plugin_name, remote_slot->plugin);
+		dbid = get_database_oid(remote_slot->database, false);
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = dbid;
+		slot->data.plugin = plugin_name;
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 8
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info from the primary server: %s",
+					   res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, 3, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 4, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, 5, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 7, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, 8, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+		some_slot_updated |= synchronize_one_slot(wrconn, remote_slot);
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the primary server: %s",
+					   PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR,
+			 "failed to fetch tuple for the primary server slot specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of a parameter change"));
+
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for slotsync worker.
+ *
+ * Called on slotsync worker exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the startup process, or when there is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index f2781d0455..f07147a68a 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -47,6 +47,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +104,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +250,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -263,6 +264,16 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 
 	ReplicationSlotValidateName(name, ERROR);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 */
+	if (RecoveryInProgress() && failover)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " created on the standby"));
+
 	/*
 	 * If some other backend ran this code concurrently with us, we'd likely
 	 * both allocate the same slot, and that would be bad.  We'd also be at
@@ -315,6 +326,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +692,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +721,29 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter slots to enable failover on the standby
+		 * as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot enable failover for a replication slot"
+						   " on the standby"));
+	}
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +756,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +912,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index eb685089b3..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -700,7 +702,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	XLogRecPtr	src_restart_lsn;
 	bool		src_islogical;
 	bool		temporary;
-	bool		failover;
 	char	   *plugin;
 	Datum		values[2];
 	bool		nulls[2];
@@ -756,7 +757,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	src_islogical = SlotIsLogical(&first_slot_contents);
 	src_restart_lsn = first_slot_contents.data.restart_lsn;
 	temporary = (first_slot_contents.data.persistency == RS_TEMPORARY);
-	failover = first_slot_contents.data.failover;
 	plugin = logical_slot ? NameStr(first_slot_contents.data.plugin) : NULL;
 
 	/* Check type of replication slot */
@@ -791,12 +791,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
-										failover,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..f753aed345 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e6e4bbdfb9..10e6a1e951 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11123,9 +11123,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 72ef4859ee..41f0cc35f3 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for standbys)? Only
 	 * relevant for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..1055573cde 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t",
+	'logical slot has synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 57884d3fec..cb43ba1c3f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 513c7702ff..d527bf918b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2585,6 +2586,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.30.0.windows.2



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 03:43  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  2 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-25 03:43 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wed, Jan 24, 2024 at 5:17 PM shveta malik <[email protected]> wrote:
>
> PFA v67. Note that the GUC (enable_syncslot) name is unchanged. Once
> we have final agreement on the name, we can make the change in the
> next version.
>
> Changes in v67 are:
>
> 1) Addressed comments by Peter given in [1].
> 2) Addressed comments by Swada-San given in [2].
> 3) Removed syncing 'failover' on standby from remote_slot. The
> 'failover' field will be false for synced slots. Since we do not
> support sync to cascading standbys yet, thus failover=true was
> misleading and unused there.
>

But what will happen after the standby is promoted? After promotion,
ideally, it should have failover enabled, so that the slots can be
synced. Also, note that corresponding subscriptions still have the
failover flag enabled. I think we should copy the 'failover' option
for the synced slots.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-25 04:24  Zhijie Hou (Fujitsu) <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-25 04:24 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

On Wednesday, January 24, 2024 1:11 PM Masahiko Sawada <[email protected]> wrote:
> Here are random comments on slotsyncworker.c (v66):

Thanks for the comments:

> 
> ---
> +               elog(ERROR,
> +                    "cannot synchronize local slot \"%s\" LSN(%X/%X)"
> +                    " to remote slot's LSN(%X/%X) as synchronization"
> +                    " would move it backwards", remote_slot->name,
> 
> Many error messages in slotsync.c are splitted into several lines, but I think it
> would reduce the greppability when the user looks for the error message in the
> source code.

Thanks for the suggestion! we combined most of the messages in the new version
patch. Although some messages including the above one were kept splitted,
because It's too long(> 120 col including the indent) to fit into the screen,
so I feel it's better to keep these messages splitted.

Best Regards,
Hou zj


^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 05:08  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  2 siblings, 1 reply; 119+ messages in thread

From: Peter Smith @ 2024-01-25 05:08 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Here are some review comments for v67-0002.

======
src/backend/replication/logical/slotsync.c

1.
+/* The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000 /* 30s */
+
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;

In my previous review for this, I meant for there to be no whitespace
between the #defines and the static long sleep_ms so the prior comment
then clearly belongs to all 3 lines

~~~

2. synchronize_one_slot

+ /*
+ * Sanity check: Make sure that concerned WAL is received and flushed
+ * before syncing slot to target lsn received from the primary server.
+ *
+ * This check should never pass as on the primary server, we have waited
+ * for the standby's confirmation before updating the logical slot.
+ */
+ latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+ if (remote_slot->confirmed_lsn > latestFlushPtr)
+ {
+ ereport(LOG,
+ errmsg("skipping slot synchronization as the received slot sync"
+    " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+    LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+    remote_slot->name,
+    LSN_FORMAT_ARGS(latestFlushPtr)));
+
+ return false;
+ }

Previously in v65 this was an elog, but now it is an ereport. But
since this is a sanity check condition that "should never pass" wasn't
the elog the more appropriate choice?

~~~

3. synchronize_one_slot

+ /*
+ * We don't want any complicated code while holding a spinlock, so do
+ * namestrcpy() and get_database_oid() outside.
+ */
+ namestrcpy(&plugin_name, remote_slot->plugin);
+ dbid = get_database_oid(remote_slot->database, false);

IMO just simplify the whole comment, here and for the other similar
comment in local_slot_update().

SUGGESTION
/* Avoid expensive operations while holding a spinlock. */

~~~

4. synchronize_slots

+ /* Construct the remote_slot tuple and synchronize each slot locally */
+ tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+ {
+ bool isnull;
+ RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+ Datum d;
+
+ remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, 1, &isnull));
+ Assert(!isnull);
+
+ remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, 2, &isnull));
+ Assert(!isnull);
+
+ /*
+ * It is possible to get null values for LSN and Xmin if slot is
+ * invalidated on the primary server, so handle accordingly.
+ */
+ d = slot_getattr(tupslot, 3, &isnull);
+ remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+ DatumGetLSN(d);
+
+ d = slot_getattr(tupslot, 4, &isnull);
+ remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+ d = slot_getattr(tupslot, 5, &isnull);
+ remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+ DatumGetTransactionId(d);
+
+ remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, 6, &isnull));
+ Assert(!isnull);
+
+ remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+ 7, &isnull));
+ Assert(!isnull);
+
+ d = slot_getattr(tupslot, 8, &isnull);
+ remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+ get_slot_invalidation_cause(TextDatumGetCString(d));

Would it be better to get rid of the hardwired column numbers and then
be able to use the SLOTSYNC_COLUMN_COUNT already defined as a sanity
check?

SUGGESTION
int col = 0;
...
remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, ++col, &isnull));
...
remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, ++col,
&isnull));
...
d = slot_getattr(tupslot, ++col, &isnull);
remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
...
d = slot_getattr(tupslot, ++col, &isnull);
remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
...
d = slot_getattr(tupslot, ++col, &isnull);
remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
DatumGetTransactionId(d);
...
remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, ++col, &isnull));
...
remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
++col, &isnull));
...
d = slot_getattr(tupslot, ++col, &isnull);
remote_slot->invalidated = isnull ? RS_INVAL_NONE :
get_slot_invalidation_cause(TextDatumGetCString(d));

/* Sanity check */
Asert(col == SLOTSYNC_COLUMN_COUNT);

~~~

5.
+static char *
+validate_parameters_and_get_dbname(void)
+{
+ char    *dbname;

These are configuration issues, so probably all these ereports could
also set errcode(ERRCODE_INVALID_PARAMETER_VALUE).

======
Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 05:56  Bertrand Drouvot <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-25 05:56 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>

Hi,

On Wed, Jan 24, 2024 at 04:09:15PM +0530, shveta malik wrote:
> On Wed, Jan 24, 2024 at 2:38 PM Bertrand Drouvot
> <[email protected]> wrote:
> >
> > I also see Sawada-San's point and I'd vote for "sync_replication_slots". Then for
> > the current feature I think "failover" and "on" should be the values to turn the
> > feature on (assuming "on" would mean "all kind of supported slots").
> 
> Even if others agree and we change this GUC name to
> "sync_replication_slots", I feel we should keep the values as "on" and
> "off" currently, where "on" would mean 'sync failover slots' (docs can
> state that clearly).

I gave more thoughts on it and I think the values should only be "failover" or
"off".

The reason is that if we allow "on" and change the "on" behavior in future
versions (to support more than failover slots) then that would change the behavior 
for the ones that used "on".

That's right that we can mention it in the docs, but there is still the risk of
users not reading the doc (that's why I think that it would be good if we can put 
this extra "safety" in the code too).

>  I do not think we should support sync of "all
> kinds of supported slots" in the first version. Maybe we can think
> about it for future versions.

Yeah I think the same (I was mentioning the future "on" behavior up-thread).

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 06:11  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-25 06:11 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Thu, Jan 25, 2024 at 9:13 AM Amit Kapila <[email protected]> wrote:
>
> > 3) Removed syncing 'failover' on standby from remote_slot. The
> > 'failover' field will be false for synced slots. Since we do not
> > support sync to cascading standbys yet, thus failover=true was
> > misleading and unused there.
> >
>
> But what will happen after the standby is promoted? After promotion,
> ideally, it should have failover enabled, so that the slots can be
> synced. Also, note that corresponding subscriptions still have the
> failover flag enabled. I think we should copy the 'failover' option
> for the synced slots.

Yes, right, missed this point earlier. I will make the change in the
next version.

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 07:55  Bertrand Drouvot <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-25 07:55 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Hi,

On Thu, Jan 25, 2024 at 02:57:30AM +0000, Zhijie Hou (Fujitsu) wrote:
> On Wednesday, January 24, 2024 6:31 PM Amit Kapila <[email protected]> wrote:
> > 
> > On Tue, Jan 23, 2024 at 5:13 PM shveta malik <[email protected]> wrote:
> > >
> > > Thanks Ajin for testing the patch. PFA v66 which fixes this issue.
> > >
> > 
> > I think we should try to commit the patch as all of the design concerns are
> > resolved now. To achieve that, can we split the failover setting patch into the
> > following: (a) setting failover property via SQL commands and display it in
> > pg_replication_slots (b) replication protocol command (c) failover property via
> > subscription commands?
> > 
> > It will make each patch smaller and it would be easier to detect any problem in
> > the same after commit.
> 
> Agreed. I split the original 0001 patch into 3 patches as suggested.
> Here is the V68 patch set.

Thanks!

Some comments.

Looking at 0002:

1 ===

+      <para>The following options are supported:</para>

What about "The following option is supported"? (as currently only the "FAILOVER"
is)

2 ===

What about adding some TAP tests too? (I can see that ALTER_REPLICATION_SLOT test
is added in v68-0004 but I think having some in 0002 would make sense too).

Looking at 0003:

1 ===

+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription.

What about explaining what would be the consequence of not doing so?

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 10:24  Amit Kapila <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-25 10:24 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Thu, Jan 25, 2024 at 1:25 PM Bertrand Drouvot
<[email protected]> wrote:
>
> On Thu, Jan 25, 2024 at 02:57:30AM +0000, Zhijie Hou (Fujitsu) wrote:
> >
> > Agreed. I split the original 0001 patch into 3 patches as suggested.
> > Here is the V68 patch set.

Thanks, I have pushed 0001.

>
> Thanks!
>
> Some comments.
>
> Looking at 0002:
>
> 1 ===
>
> +      <para>The following options are supported:</para>
>
> What about "The following option is supported"? (as currently only the "FAILOVER"
> is)
>
> 2 ===
>
> What about adding some TAP tests too? (I can see that ALTER_REPLICATION_SLOT test
> is added in v68-0004 but I think having some in 0002 would make sense too).
>

The subscription tests in v68-0003 will test this functionality. The
one advantage of adding separate tests for this is that if in the
future we extend this replication command, it could be convenient to
test various options. However, the same could be said about existing
replication commands as well. But is it worth having extra tests which
will be anyway covered in the next commit in a few days?

I understand that it is a good idea and makes one comfortable to have
tests for each separate commit but OTOH, in the longer term it will
just be adding more test time without achieving much benefit. I think
we can tell explicitly in the commit message of this patch that the
subsequent commit will cover the tests for this functionality

One minor comment on 0002:
+          so that logical replication can be resumed after failover.
+         </para>

Can we move this and similar comments or doc changes to the later 0004
patch where we are syncing the slots?


-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 11:47  shveta malik <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: shveta malik @ 2024-01-25 11:47 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Ajin Cherian <[email protected]>; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Thu, Jan 25, 2024 at 10:39 AM Peter Smith <[email protected]> wrote:

> 2. synchronize_one_slot
>
> + /*
> + * Sanity check: Make sure that concerned WAL is received and flushed
> + * before syncing slot to target lsn received from the primary server.
> + *
> + * This check should never pass as on the primary server, we have waited
> + * for the standby's confirmation before updating the logical slot.
> + */
> + latestFlushPtr = GetStandbyFlushRecPtr(NULL);
> + if (remote_slot->confirmed_lsn > latestFlushPtr)
> + {
> + ereport(LOG,
> + errmsg("skipping slot synchronization as the received slot sync"
> +    " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
> +    LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
> +    remote_slot->name,
> +    LSN_FORMAT_ARGS(latestFlushPtr)));
> +
> + return false;
> + }
>
> Previously in v65 this was an elog, but now it is an ereport. But
> since this is a sanity check condition that "should never pass" wasn't
> the elog the more appropriate choice?

We realized that this scenario can be frequently hit when the user has
not set standby_slot_names on primary. And thus ereport makes more
sense. But I agree that this comment is misleading. We will adjust the
comment in the next version.

thanks
Shveta





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-25 11:51  Bertrand Drouvot <[email protected]>
  parent: Amit Kapila <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-25 11:51 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Hi,

On Thu, Jan 25, 2024 at 03:54:45PM +0530, Amit Kapila wrote:
> On Thu, Jan 25, 2024 at 1:25 PM Bertrand Drouvot
> <[email protected]> wrote:
> >
> > On Thu, Jan 25, 2024 at 02:57:30AM +0000, Zhijie Hou (Fujitsu) wrote:
> > >
> > > Agreed. I split the original 0001 patch into 3 patches as suggested.
> > > Here is the V68 patch set.
> 
> Thanks, I have pushed 0001.
> 
> >
> > Thanks!
> >
> > Some comments.
> >
> > Looking at 0002:
> >
> > 1 ===
> >
> > +      <para>The following options are supported:</para>
> >
> > What about "The following option is supported"? (as currently only the "FAILOVER"
> > is)
> >
> > 2 ===
> >
> > What about adding some TAP tests too? (I can see that ALTER_REPLICATION_SLOT test
> > is added in v68-0004 but I think having some in 0002 would make sense too).
> >
> 
> The subscription tests in v68-0003 will test this functionality. The
> one advantage of adding separate tests for this is that if in the
> future we extend this replication command, it could be convenient to
> test various options. However, the same could be said about existing
> replication commands as well.

I initially did check for "START_REPLICATION" and I saw it's part of 
006_logical_decoding.pl (but did not check all the "REPLICATION" commands).

That said, it's more a Nit and I think it's fine with having the test in v68-0004
(as it is currently done) + the ones in v68-0003.

> But is it worth having extra tests which
> will be anyway covered in the next commit in a few days?
> 
> I understand that it is a good idea and makes one comfortable to have
> tests for each separate commit but OTOH, in the longer term it will
> just be adding more test time without achieving much benefit. I think
> we can tell explicitly in the commit message of this patch that the
> subsequent commit will cover the tests for this functionality

Yeah, I think that's enough (at least someone reading the commit message, the 
diff changes and not following this dedicated thread closely would know the lack
of test is not a miss).

> One minor comment on 0002:
> +          so that logical replication can be resumed after failover.
> +         </para>
> 
> Can we move this and similar comments or doc changes to the later 0004
> patch where we are syncing the slots?

Sure.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-25 13:11  Zhijie Hou (Fujitsu) <[email protected]>
  parent: Amit Kapila <[email protected]>
  1 sibling, 2 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-25 13:11 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; +Cc: shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Thursday, January 25, 2024 6:25 PM Amit Kapila <[email protected]> wrote:
> 
> On Thu, Jan 25, 2024 at 1:25 PM Bertrand Drouvot
> <[email protected]> wrote:
> >
> > On Thu, Jan 25, 2024 at 02:57:30AM +0000, Zhijie Hou (Fujitsu) wrote:
> > >
> > > Agreed. I split the original 0001 patch into 3 patches as suggested.
> > > Here is the V68 patch set.
> 
> Thanks, I have pushed 0001.
> 
> >
> > Thanks!
> >
> > Some comments.
> >
> > Looking at 0002:
> >
> > 1 ===
> >
> > +      <para>The following options are supported:</para>
> >
> > What about "The following option is supported"? (as currently only the
> "FAILOVER"
> > is)
> >
> > 2 ===
> >
> > What about adding some TAP tests too? (I can see that
> > ALTER_REPLICATION_SLOT test is added in v68-0004 but I think having some
> in 0002 would make sense too).
> >
> 
> The subscription tests in v68-0003 will test this functionality. The one
> advantage of adding separate tests for this is that if in the future we extend this
> replication command, it could be convenient to test various options. However,
> the same could be said about existing replication commands as well. But is it
> worth having extra tests which will be anyway covered in the next commit in a
> few days?
> 
> I understand that it is a good idea and makes one comfortable to have tests for
> each separate commit but OTOH, in the longer term it will just be adding more
> test time without achieving much benefit. I think we can tell explicitly in the
> commit message of this patch that the subsequent commit will cover the tests
> for this functionality

Agreed.

> 
> One minor comment on 0002:
> +          so that logical replication can be resumed after failover.
> +         </para>
> 
> Can we move this and similar comments or doc changes to the later 0004 patch
> where we are syncing the slots?

Thanks for the comment.

Here is the V69 patch set which includes the following changes.

V69-0001, V69-0002
1) Addressed Bertrand's comments[1].

V69-0003
1) Addressed Peter's comment in [2], [3]
2) Addressed Amit's comment in [4] and above.
3) Fixed one issue that the startup process may report ERROR if it tries to drop
the same slot that the slotsync worker is acquiring. Now we take shared lock on
db in slot-sync worker before we create, update or drop any of its slots. This
is done to prevent potential conflict with ReplicationSlotsDropDBSlots() in
case that database is dropped in parallel.

V69-0004
1) Rebased and fixed one CFbot failure.

V69-0005, V69-0006, V69-0007
1) Rebased.

Thanks Shveta for rebasing and working for the changes on 0003~0007.

[1] https://www.postgresql.org/message-id/ZbIT9Kj3d8TFD8h6%40ip-10-97-1-34.eu-west-3.compute.internal
[2]: https://www.postgresql.org/message-id/CAHut%2BPt2oLfxv_%3DGN23dOOduKHBHdAkCvwSZiwSbtTJFFbQm-w%40mail...
[3]: https://www.postgresql.org/message-id/CAHut%2BPtsDYPbg7qM1nGWtJcSQBQ5JH%3DLmgyqwqBPL9k%2Bz8f5Ew%40ma...
[4]: https://www.postgresql.org/message-id/CAA4eK1%2B4PhO-f4%2B2fForG6MOEj3jbtee_PYPtwtgww%3DonC5DSQ%40ma...

Best Regards,
Hou zj


Attachments:

  [application/octet-stream] v69-0006-Non-replication-connection-and-app_name-change.patch (9.7K, ../../OS0PR01MB571623B1D01003F99A7BB983947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/2-v69-0006-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 2ad7abfdfe3eff6a1e2a6310267ffc8dcd03f107 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v69 6/7] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 15bb10da54..f17c666ebc 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1531,7 +1533,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1782,7 +1785,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index ae76c098b1..13a787b8bf 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 68ad8bbcbc..9afc2a0381 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1096,6 +1096,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1205,13 +1206,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..0d791c188c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.30.0.windows.2



  [application/octet-stream] v69-0007-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../OS0PR01MB571623B1D01003F99A7BB983947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/3-v69-0007-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From d27c838de3b5713d46d95c3eafaadaac226ff80c Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v69 7/7] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.30.0.windows.2



  [application/octet-stream] v69-0003-Add-logical-slot-sync-capability-to-the-physical.patch (91.4K, ../../OS0PR01MB571623B1D01003F99A7BB983947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/4-v69-0003-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 45463ca3eb9fdb0907a15e16a60064e5d976c036 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Thu, 25 Jan 2024 20:28:30 +0800
Subject: [PATCH v69 3/7] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/protocol.sgml                    |    6 +-
 doc/src/sgml/system-views.sgml                |   22 +-
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1222 +++++++++++++++++
 src/backend/replication/slot.c                |   59 +-
 src/backend/replication/slotfuncs.c           |   26 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 35 files changed, 1792 insertions(+), 49 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index bb4fef1f51..f8ef2ad2ab 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2065,7 +2065,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
           The default is false.
          </para>
         </listitem>
@@ -2165,7 +2166,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
          </para>
         </listitem>
        </varlistentry>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index dd468b31ea..4ea2177b34 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2561,10 +2561,28 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        <structfield>failover</structfield> <type>bool</type>
       </para>
       <para>
-       True if this is a logical slot enabled to be synced to the standbys.
-       Always false for physical slots.
+       True if this is a logical slot enabled to be synced to the standbys
+       so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bb472da27..87b49d524a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1467,6 +1468,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6fc3916850..2e8ce9d554 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 20d5128c0e..ae76c098b1 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..751d03f5b9
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1222 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/lmgr.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+	NameData	plugin_name;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	/* Avoid expensive operations while holding a spinlock. */
+	namestrcpy(&plugin_name, remote_slot->plugin);
+
+	SpinLockAcquire(&slot->mutex);
+	slot->data.plugin = plugin_name;
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			/*
+			 * Use shared lock to prevent a conflict with
+			 * ReplicationSlotsDropDBSlots(), trying to drop the same slot
+			 * while drop-database operation.
+			 */
+			LockSharedObject(DatabaseRelationId, local_slot->data.database,
+							 0, AccessShareLock);
+
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			UnlockSharedObject(DatabaseRelationId, local_slot->data.database,
+							   0, AccessShareLock);
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot, remote_dbid))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Make sure that concerned WAL is received and flushed before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check will never pass if on the primary server, user has
+	 * configured standby_slot_names GUC correctly, otherwise this can hit
+	 * frequently.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization as the received slot sync"
+					   " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+					   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+					   remote_slot->name,
+					   LSN_FORMAT_ARGS(latestFlushPtr)));
+
+		return false;
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot, remote_dbid);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot, remote_dbid))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		NameData	plugin_name;
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		/* Avoid expensive operations while holding a spinlock. */
+		namestrcpy(&plugin_name, remote_slot->plugin);
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = remote_dbid;
+		slot->data.plugin = plugin_name;
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot, remote_dbid);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info from the primary server: %s",
+					   res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+		int			col = 0;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															 &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															   &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, ++col,
+														   &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, ++col,
+														  &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 ++col, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Sanity check */
+		Assert(col == SLOTSYNC_COLUMN_COUNT);
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+	{
+		Oid			remote_dbid = get_database_oid(remote_slot->database, false);
+
+		/*
+		 * Use shared lock to prevent a conflict with
+		 * ReplicationSlotsDropDBSlots(), trying to drop the same slot while
+		 * drop-database operation.
+		 */
+		LockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+
+		some_slot_updated |= synchronize_one_slot(remote_slot, remote_dbid);
+
+		UnlockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+	}
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the primary server: %s",
+					   PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR,
+			 "failed to fetch tuple for the primary server slot specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of a parameter change"));
+
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for slotsync worker.
+ *
+ * Called on slotsync worker exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the startup process, or when there is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index f2781d0455..8caa6a9e09 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,7 +46,9 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +105,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +251,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -263,6 +265,19 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 
 	ReplicationSlotValidateName(name, ERROR);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 *
+	 * Slot sync worker can still create slots with failover enabled, as it
+	 * needs to maintain this value in sync with the remote slots.
+	 */
+	if (failover && RecoveryInProgress() && !IsLogicalSlotSyncWorker())
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " created on the standby"));
+
 	/*
 	 * If some other backend ran this code concurrently with us, we'd likely
 	 * both allocate the same slot, and that would be bad.  We'd also be at
@@ -315,6 +330,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +696,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +725,29 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter slots to enable failover on the standby
+		 * as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot enable failover for a replication slot"
+						   " on the standby"));
+	}
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +760,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +916,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index eb685089b3..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -700,7 +702,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	XLogRecPtr	src_restart_lsn;
 	bool		src_islogical;
 	bool		temporary;
-	bool		failover;
 	char	   *plugin;
 	Datum		values[2];
 	bool		nulls[2];
@@ -756,7 +757,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	src_islogical = SlotIsLogical(&first_slot_contents);
 	src_restart_lsn = first_slot_contents.data.restart_lsn;
 	temporary = (first_slot_contents.data.persistency == RS_TEMPORARY);
-	failover = first_slot_contents.data.failover;
 	plugin = logical_slot ? NameStr(first_slot_contents.data.plugin) : NULL;
 
 	/* Check type of replication slot */
@@ -791,12 +791,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
-										failover,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..f753aed345 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..994e33f59b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11127,9 +11127,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index da4c776492..1f4446aa3a 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for standbys)? Only relevant
 	 * for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..1055573cde 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t",
+	'logical slot has synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index abc944e8b8..b7488d760e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 513c7702ff..d527bf918b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2585,6 +2586,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.30.0.windows.2



  [application/octet-stream] v69-0004-Slot-sync-worker-as-a-special-process.patch (38.4K, ../../OS0PR01MB571623B1D01003F99A7BB983947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/5-v69-0004-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From a8e7eca273c412a65f61eaaf97655744091fae51 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 12:54:09 +0530
Subject: [PATCH v69 4/7] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  86 +++--
 src/backend/replication/logical/slotsync.c    | 359 ++++++++++++++----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   8 +-
 14 files changed, 387 insertions(+), 181 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..adfaf75cf9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,10 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY && \
+	 SlotSyncWorkerCanRestart())
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1016,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1836,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2687,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3046,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3218,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3600,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3746,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3763,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3779,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3878,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4102,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4916,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5020,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5849,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 751d03f5b9..94ca93f886 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -34,15 +34,20 @@
 
 #include "postgres.h"
 
+#include <time.h>
+
 #include "access/genam.h"
 #include "access/table.h"
 #include "access/xlog_internal.h"
 #include "access/xlogrecovery.h"
 #include "catalog/pg_database.h"
 #include "commands/dbcommands.h"
+#include "libpq/pqsignal.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -56,6 +61,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -109,8 +116,16 @@ bool		enable_syncslot = false;
 #define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -734,7 +749,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -749,8 +765,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -788,13 +806,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -803,20 +824,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -824,11 +840,14 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -836,49 +855,98 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
-		/* translator: %s is a GUC variable name */
+	{
+		ereport(LOG,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -894,16 +962,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -911,8 +991,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of a parameter change"));
 
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -929,7 +1008,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
 		proc_exit(0);
@@ -961,17 +1041,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -1007,20 +1086,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -1035,26 +1132,77 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	/*
+	 * Unblock signals (they were blocked when the postmaster forked us)
+	 */
+	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1073,26 +1221,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1108,7 +1259,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1138,7 +1289,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1181,42 +1332,92 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * SlotSyncWorkerCanRestart
+ *
+ * Returns true if the worker is allowed to restart if enough time has
+ * passed (SLOTSYNC_RESTART_INTERVAL_SEC) since it was launched last.
+ * Otherwise returns false.
+ *
+ * This is a safety valve to protect against continuous respawn attempts if the
+ * worker is dying immediately at launch. Note that since we will retry to
+ * launch the worker from the postmaster main loop, we will get another
+ * chance later.
+ */
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+	static time_t last_slotsync_start_time = 0;
+	time_t		curtime = time(NULL);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+	/* Return false if too soon since last start. */
+	if ((unsigned int) (curtime - last_slotsync_start_time) <
+		(unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC)
+		return false;
+
+	last_slotsync_start_time = curtime;
+	return true;
+}
+
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
+
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
+
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87..aa53a57077 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..aa01f63648 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,11 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
+extern bool SlotSyncWorkerCanRestart(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.30.0.windows.2



  [application/octet-stream] v69-0005-Allow-logical-walsenders-to-wait-for-the-physica.patch (41.2K, ../../OS0PR01MB571623B1D01003F99A7BB983947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/6-v69-0005-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From acf0300d241b834790661449b2b8dd1499faba83 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 14:28:42 +0530
Subject: [PATCH v69 5/7] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/logical/slotsync.c    |   1 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 17 files changed, 739 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 94ca93f886..68ad8bbcbc 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -44,6 +44,7 @@
 #include "commands/dbcommands.h"
 #include "libpq/pqsignal.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "postmaster/bgworker.h"
 #include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 8caa6a9e09..8dc2bc7c95 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,14 +46,19 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -100,10 +105,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2237,3 +2251,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index f753aed345..71933f4bf1 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 1f4446aa3a..1054910cac 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 1055573cde..06a4ada941 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.30.0.windows.2



  [application/octet-stream] v69-0002-Add-a-failover-option-to-subscriptions.patch (63.8K, ../../OS0PR01MB571623B1D01003F99A7BB983947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/7-v69-0002-Add-a-failover-option-to-subscriptions.patch)
  download | inline diff:
From 9e9208f0c0fa625166dd5580254bd644ae636fab Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:51:39 +0800
Subject: [PATCH v69 2/7] Add a failover option to subscriptions.

This commit introduces a new subscription option named 'failover', which
provides users with the ability to set the failover property of the replication
slot on the publisher when creating or altering a subscription.
---
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/ref/alter_subscription.sgml      |  17 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_views.sql          |   3 +-
 src/backend/commands/subscriptioncmds.c       | 106 ++++++++++-
 src/backend/replication/logical/tablesync.c   |   2 +-
 src/backend/replication/logical/worker.c      |   7 +
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_subscription.h         |   9 +
 .../t/050_standby_failover_slots_sync.pl      |  89 ++++++++++
 src/test/regress/expected/subscription.out    | 165 ++++++++++--------
 src/test/regress/sql/subscription.sql         |   8 +
 17 files changed, 376 insertions(+), 91 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16b94461b2..880f717b10 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -8000,6 +8000,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..0c34ab9017 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,23 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription. Otherwise, the slot on the publisher may
+      not be enabled to be synced to standbys.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..59a9554bf0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index c62aa0074a..6fc3916850 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1359,7 +1359,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eaf2ec3b36..15bb10da54 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1513,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4207b9356c..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,7 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
-					   false,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd..19a3865699 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 93d97a4090..77db42e354 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -667,6 +667,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9cd8783325..b6a4eb1d56 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6571,7 +6571,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6635,6 +6636,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..4d0e364624 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -148,6 +153,10 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..5fa230a895 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +412,31 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..e4601158b3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,14 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
-- 
2.30.0.windows.2



  [application/octet-stream] v69-0001-Allow-setting-failover-property-in-the-replicati.patch (19.5K, ../../OS0PR01MB571623B1D01003F99A7BB983947A2@OS0PR01MB5716.jpnprd01.prod.outlook.com/8-v69-0001-Allow-setting-failover-property-in-the-replicati.patch)
  download | inline diff:
From 8fb41054bfe9b04dd4244734bcf79db29c5f7180 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:04:18 +0800
Subject: [PATCH v69 1/7] Allow setting failover property in the replication
 command.

This commit implements a new replication command called ALTER_REPLICATION_SLOT
and a corresponding walreceiver API function named walrcv_alter_slot.
Additionally, the CREATE_REPLICATION_SLOT command has been extended to support
the failover option.

These new additions allow the modification of the failover property of a
replication slot on the publisher. A subsequent commit will make use of these
commands in subscription commands.
---
 doc/src/sgml/protocol.sgml                    | 50 +++++++++++++++
 src/backend/commands/subscriptioncmds.c       |  2 +-
 .../libpqwalreceiver/libpqwalreceiver.c       | 38 +++++++++++-
 src/backend/replication/logical/tablesync.c   |  1 +
 src/backend/replication/repl_gram.y           | 20 +++++-
 src/backend/replication/repl_scanner.l        |  2 +
 src/backend/replication/slot.c                | 25 ++++++++
 src/backend/replication/walreceiver.c         |  2 +-
 src/backend/replication/walsender.c           | 62 ++++++++++++++++++-
 src/include/nodes/replnodes.h                 | 12 ++++
 src/include/replication/slot.h                |  1 +
 src/include/replication/walreceiver.h         | 18 +++++-
 src/tools/pgindent/typedefs.list              |  2 +
 13 files changed, 223 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..bb4fef1f51 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,16 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2134,46 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following option is supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..eaf2ec3b36 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -807,7 +807,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 77669074e8..20d5128c0e 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -73,8 +73,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -95,6 +98,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -938,8 +942,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -968,7 +972,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 			else
 				appendStringInfoChar(&cmd, ' ');
 		}
-
+		if (failover)
+			appendStringInfoString(&cmd, "FAILOVER, ");
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -1037,6 +1042,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..4207b9356c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   false,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..ff3809e02f 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 02a14ec210..f2781d0455 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -683,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, false);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 728059518e..e29a6196a3 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index aa80f3de20..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,7 +1217,8 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, false);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index db9bb22266..da4c776492 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -227,6 +227,7 @@ extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..513c7702ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3880,6 +3881,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.30.0.windows.2



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-26 08:31  Bertrand Drouvot <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-26 08:31 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Hi,

On Thu, Jan 25, 2024 at 01:11:50PM +0000, Zhijie Hou (Fujitsu) wrote:
> Here is the V69 patch set which includes the following changes.
> 
> V69-0001, V69-0002
> 1) Addressed Bertrand's comments[1].

Thanks!

V69-0001 LGTM.

As far V69-0002 I just have one more last remark:

+                                        */
+                                       if (sub->enabled)
+                                               ereport(ERROR,
+                                                               (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                                                                errmsg("cannot set %s for enabled subscription",
+                                                                               "failover")));

Worth to add a test for it in 050_standby_failover_slots_sync.pl? (I had a quick
look and it does not seem to be covered).

Remarks,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-27 03:43  Amit Kapila <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-27 03:43 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Thu, Jan 25, 2024 at 6:42 PM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> Here is the V69 patch set which includes the following changes.
>
> V69-0001, V69-0002
>

Few minor comments on v69-0001
1. In libpqrcv_create_slot(), I see we are using two types of syntaxes
based on 'use_new_options_syntax' (aka server_version >= 15) whereas
this new 'failover' option doesn't follow that. What is the reason of
the same? I thought it is because older versions anyway won't support
this option. However, I guess we should follow the syntax of the old
server and let it error out. BTW, did you test this patch with old
server versions (say < 15 and >=15) by directly using replication
commands, if so, what is the behavior of same?

2.
  }
-
+ if (failover)
+ appendStringInfoString(&cmd, "FAILOVER, ");

Spurious line removal. Also, to follow a coding pattern similar to
nearby code, let's have one empty line after handling of failover.

3.
+/* ALTER_REPLICATION_SLOT slot */
+alter_replication_slot:
+ K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'

I think it would be better if we follow the create style by specifying
syntax in comments as that can make the code easier to understand
after future extensions to this command if any. See
create_replication_slot:
/* CREATE_REPLICATION_SLOT slot [TEMPORARY] PHYSICAL [options] */
K_CREATE_REPLICATION_SLOT IDENT opt_temporary K_PHYSICAL create_slot_options

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-27 06:31  Zhijie Hou (Fujitsu) <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-27 06:31 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Saturday, January 27, 2024 11:43 AM Amit Kapila <[email protected]> wrote:
> 
> On Thu, Jan 25, 2024 at 6:42 PM Zhijie Hou (Fujitsu) <[email protected]>
> wrote:
> >
> > Here is the V69 patch set which includes the following changes.
> >
> > V69-0001, V69-0002
> >
> 
> Few minor comments on v69-0001
> 1. In libpqrcv_create_slot(), I see we are using two types of syntaxes based on
> 'use_new_options_syntax' (aka server_version >= 15) whereas this new 'failover'
> option doesn't follow that. What is the reason of the same? I thought it is
> because older versions anyway won't support this option. However, I guess we
> should follow the syntax of the old server and let it error out.

Changed as suggested.

> BTW, did you test
> this patch with old server versions (say < 15 and >=15) by directly using
> replication commands, if so, what is the behavior of same?

Yes, I tested it. We cannot use new failover option or new
alter_replication_slot on server <17, the errors we will get are as follows:

Using failover option in create_replication_slot on server 15 ~ 16
	ERROR:  unrecognized option: failover
Using failover option in create_replication_slot on server < 15
	ERROR:  syntax error
Alter_replication_slot on server < 17
	ERROR:  syntax error at or near "ALTER_REPLICATION_SLOT"


> 
> 2.
>   }
> -
> + if (failover)
> + appendStringInfoString(&cmd, "FAILOVER, ");
> 
> Spurious line removal. Also, to follow a coding pattern similar to nearby code,
> let's have one empty line after handling of failover.

Changed.

> 
> 3.
> +/* ALTER_REPLICATION_SLOT slot */
> +alter_replication_slot:
> + K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
> 
> I think it would be better if we follow the create style by specifying syntax in
> comments as that can make the code easier to understand after future
> extensions to this command if any. See
> create_replication_slot:
> /* CREATE_REPLICATION_SLOT slot [TEMPORARY] PHYSICAL [options] */
> K_CREATE_REPLICATION_SLOT IDENT opt_temporary K_PHYSICAL
> create_slot_options

Changed.

Attach the V70 patch set which addressed above comments and Bertrand's comments in [1]

[1] https://www.postgresql.org/message-id/ZbNt1oRZRcdIAw2c%40ip-10-97-1-34.eu-west-3.compute.internal

Best Regards,
Hou zj


Attachments:

  [application/octet-stream] v70-0006-Non-replication-connection-and-app_name-change.patch (9.7K, ../../OS0PR01MB5716DE2A364CB23F9144AED194782@OS0PR01MB5716.jpnprd01.prod.outlook.com/2-v70-0006-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From af4b747ade9619b158375229cb160943d2ffbf3e Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v70 6/7] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 15bb10da54..f17c666ebc 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1531,7 +1533,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1782,7 +1785,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 61eeaa17b2..512ec2897a 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 68ad8bbcbc..9afc2a0381 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1096,6 +1096,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1205,13 +1206,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..0d791c188c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v70-0004-Slot-sync-worker-as-a-special-process.patch (38.4K, ../../OS0PR01MB5716DE2A364CB23F9144AED194782@OS0PR01MB5716.jpnprd01.prod.outlook.com/3-v70-0004-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From 18c94261b6932a2a1d260b7c163c7a677c40e145 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 12:54:09 +0530
Subject: [PATCH v70 4/7] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  86 +++--
 src/backend/replication/logical/slotsync.c    | 359 ++++++++++++++----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   8 +-
 14 files changed, 387 insertions(+), 181 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..adfaf75cf9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,10 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY && \
+	 SlotSyncWorkerCanRestart())
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1016,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1836,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2687,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3046,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3218,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3600,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3746,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3763,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3779,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3878,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4102,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4916,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5020,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5849,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 751d03f5b9..94ca93f886 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -34,15 +34,20 @@
 
 #include "postgres.h"
 
+#include <time.h>
+
 #include "access/genam.h"
 #include "access/table.h"
 #include "access/xlog_internal.h"
 #include "access/xlogrecovery.h"
 #include "catalog/pg_database.h"
 #include "commands/dbcommands.h"
+#include "libpq/pqsignal.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -56,6 +61,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -109,8 +116,16 @@ bool		enable_syncslot = false;
 #define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -734,7 +749,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -749,8 +765,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -788,13 +806,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -803,20 +824,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -824,11 +840,14 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -836,49 +855,98 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
-		/* translator: %s is a GUC variable name */
+	{
+		ereport(LOG,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -894,16 +962,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -911,8 +991,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of a parameter change"));
 
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -929,7 +1008,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
 		proc_exit(0);
@@ -961,17 +1041,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -1007,20 +1086,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -1035,26 +1132,77 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	/*
+	 * Unblock signals (they were blocked when the postmaster forked us)
+	 */
+	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1073,26 +1221,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1108,7 +1259,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1138,7 +1289,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1181,42 +1332,92 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * SlotSyncWorkerCanRestart
+ *
+ * Returns true if the worker is allowed to restart if enough time has
+ * passed (SLOTSYNC_RESTART_INTERVAL_SEC) since it was launched last.
+ * Otherwise returns false.
+ *
+ * This is a safety valve to protect against continuous respawn attempts if the
+ * worker is dying immediately at launch. Note that since we will retry to
+ * launch the worker from the postmaster main loop, we will get another
+ * chance later.
+ */
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+	static time_t last_slotsync_start_time = 0;
+	time_t		curtime = time(NULL);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+	/* Return false if too soon since last start. */
+	if ((unsigned int) (curtime - last_slotsync_start_time) <
+		(unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC)
+		return false;
+
+	last_slotsync_start_time = curtime;
+	return true;
+}
+
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
+
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
+
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe..f19e5faeaf 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..aa01f63648 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,11 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
+extern bool SlotSyncWorkerCanRestart(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v70-0007-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../OS0PR01MB5716DE2A364CB23F9144AED194782@OS0PR01MB5716.jpnprd01.prod.outlook.com/4-v70-0007-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From 0858f13203077277f7760fe0ee9d0933fe4c3417 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v70 7/7] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



  [application/octet-stream] v70-0001-Allow-setting-failover-property-in-the-replicati.patch (19.6K, ../../OS0PR01MB5716DE2A364CB23F9144AED194782@OS0PR01MB5716.jpnprd01.prod.outlook.com/5-v70-0001-Allow-setting-failover-property-in-the-replicati.patch)
  download | inline diff:
From 72db81a7d81ab90d09926ec14ec395902fe03b51 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:04:18 +0800
Subject: [PATCH v70 1/7] Allow setting failover property in the replication
 command.

This commit implements a new replication command called ALTER_REPLICATION_SLOT
and a corresponding walreceiver API function named walrcv_alter_slot.
Additionally, the CREATE_REPLICATION_SLOT command has been extended to support
the failover option.

These new additions allow the modification of the failover property of a
replication slot on the publisher. A subsequent commit will make use of these
commands in subscription commands.
---
 doc/src/sgml/protocol.sgml                    | 50 +++++++++++++++
 src/backend/commands/subscriptioncmds.c       |  2 +-
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 ++++++++++++-
 src/backend/replication/logical/tablesync.c   |  1 +
 src/backend/replication/repl_gram.y           | 20 +++++-
 src/backend/replication/repl_scanner.l        |  2 +
 src/backend/replication/slot.c                | 25 ++++++++
 src/backend/replication/walreceiver.c         |  2 +-
 src/backend/replication/walsender.c           | 62 ++++++++++++++++++-
 src/include/nodes/replnodes.h                 | 12 ++++
 src/include/replication/slot.h                |  1 +
 src/include/replication/walreceiver.h         | 18 +++++-
 src/tools/pgindent/typedefs.list              |  2 +
 13 files changed, 230 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d..bb4fef1f51 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2060,6 +2060,16 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys.
+          The default is false.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       <para>
@@ -2124,6 +2134,46 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
      </listitem>
     </varlistentry>
 
+    <varlistentry id="protocol-replication-alter-replication-slot" xreflabel="ALTER_REPLICATION_SLOT">
+     <term><literal>ALTER_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable> ( <replaceable class="parameter">option</replaceable> [, ...] )
+      <indexterm><primary>ALTER_REPLICATION_SLOT</primary></indexterm>
+     </term>
+     <listitem>
+      <para>
+       Change the definition of a replication slot.
+       See <xref linkend="streaming-replication-slots"/> for more about
+       replication slots. This command is currently only supported for logical
+       replication slots.
+      </para>
+
+      <variablelist>
+       <varlistentry>
+        <term><replaceable class="parameter">slot_name</replaceable></term>
+        <listitem>
+         <para>
+          The name of the slot to alter. Must be a valid replication slot
+          name (see <xref linkend="streaming-replication-slots-manipulation"/>).
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      <para>The following option is supported:</para>
+
+      <variablelist>
+       <varlistentry>
+        <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
+        <listitem>
+         <para>
+          If true, the slot is enabled to be synced to the standbys.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="protocol-replication-read-replication-slot">
      <term><literal>READ_REPLICATION_SLOT</literal> <replaceable class="parameter">slot_name</replaceable>
       <indexterm><primary>READ_REPLICATION_SLOT</primary></indexterm>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 75e6cd8ae3..eaf2ec3b36 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -807,7 +807,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   CRS_NOEXPORT_SNAPSHOT, NULL);
+								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 77669074e8..2439733b55 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -73,8 +73,11 @@ static char *libpqrcv_create_slot(WalReceiverConn *conn,
 								  const char *slotname,
 								  bool temporary,
 								  bool two_phase,
+								  bool failover,
 								  CRSSnapshotAction snapshot_action,
 								  XLogRecPtr *lsn);
+static void libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+								bool failover);
 static pid_t libpqrcv_get_backend_pid(WalReceiverConn *conn);
 static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn,
 									   const char *query,
@@ -95,6 +98,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_receive = libpqrcv_receive,
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
+	.walrcv_alter_slot = libpqrcv_alter_slot,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -938,8 +942,8 @@ libpqrcv_send(WalReceiverConn *conn, const char *buffer, int nbytes)
  */
 static char *
 libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
-					 bool temporary, bool two_phase, CRSSnapshotAction snapshot_action,
-					 XLogRecPtr *lsn)
+					 bool temporary, bool two_phase, bool failover,
+					 CRSSnapshotAction snapshot_action, XLogRecPtr *lsn)
 {
 	PGresult   *res;
 	StringInfoData cmd;
@@ -969,6 +973,15 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 				appendStringInfoChar(&cmd, ' ');
 		}
 
+		if (failover)
+		{
+			appendStringInfoString(&cmd, "FAILOVER");
+			if (use_new_options_syntax)
+				appendStringInfoString(&cmd, ", ");
+			else
+				appendStringInfoChar(&cmd, ' ');
+		}
+
 		if (use_new_options_syntax)
 		{
 			switch (snapshot_action)
@@ -1037,6 +1050,33 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 	return snapshot;
 }
 
+/*
+ * Change the definition of the replication slot.
+ */
+static void
+libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname,
+					bool failover)
+{
+	StringInfoData cmd;
+	PGresult   *res;
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( FAILOVER %s )",
+					 quote_identifier(slotname),
+					 failover ? "true" : "false");
+
+	res = libpqrcv_PQexec(conn->streamConn, cmd.data);
+	pfree(cmd.data);
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("could not alter replication slot \"%s\": %s",
+						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
+
+	PQclear(res);
+}
+
 /*
  * Return PID of remote backend process.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 06d5b3df33..4207b9356c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,6 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
+					   false,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y
index 95e126eb4d..9cbf06b92c 100644
--- a/src/backend/replication/repl_gram.y
+++ b/src/backend/replication/repl_gram.y
@@ -64,6 +64,7 @@ Node *replication_parse_result;
 %token K_START_REPLICATION
 %token K_CREATE_REPLICATION_SLOT
 %token K_DROP_REPLICATION_SLOT
+%token K_ALTER_REPLICATION_SLOT
 %token K_TIMELINE_HISTORY
 %token K_WAIT
 %token K_TIMELINE
@@ -80,8 +81,9 @@ Node *replication_parse_result;
 
 %type <node>	command
 %type <node>	base_backup start_replication start_logical_replication
-				create_replication_slot drop_replication_slot identify_system
-				read_replication_slot timeline_history show upload_manifest
+				create_replication_slot drop_replication_slot
+				alter_replication_slot identify_system read_replication_slot
+				timeline_history show upload_manifest
 %type <list>	generic_option_list
 %type <defelt>	generic_option
 %type <uintval>	opt_timeline
@@ -112,6 +114,7 @@ command:
 			| start_logical_replication
 			| create_replication_slot
 			| drop_replication_slot
+			| alter_replication_slot
 			| read_replication_slot
 			| timeline_history
 			| show
@@ -259,6 +262,18 @@ drop_replication_slot:
 				}
 			;
 
+/* ALTER_REPLICATION_SLOT slot options */
+alter_replication_slot:
+			K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+				{
+					AlterReplicationSlotCmd *cmd;
+					cmd = makeNode(AlterReplicationSlotCmd);
+					cmd->slotname = $2;
+					cmd->options = $4;
+					$$ = (Node *) cmd;
+				}
+			;
+
 /*
  * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d]
  */
@@ -410,6 +425,7 @@ ident_or_keyword:
 			| K_START_REPLICATION			{ $$ = "start_replication"; }
 			| K_CREATE_REPLICATION_SLOT	{ $$ = "create_replication_slot"; }
 			| K_DROP_REPLICATION_SLOT		{ $$ = "drop_replication_slot"; }
+			| K_ALTER_REPLICATION_SLOT		{ $$ = "alter_replication_slot"; }
 			| K_TIMELINE_HISTORY			{ $$ = "timeline_history"; }
 			| K_WAIT						{ $$ = "wait"; }
 			| K_TIMELINE					{ $$ = "timeline"; }
diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l
index 6fa625617b..e7def80065 100644
--- a/src/backend/replication/repl_scanner.l
+++ b/src/backend/replication/repl_scanner.l
@@ -125,6 +125,7 @@ TIMELINE			{ return K_TIMELINE; }
 START_REPLICATION	{ return K_START_REPLICATION; }
 CREATE_REPLICATION_SLOT		{ return K_CREATE_REPLICATION_SLOT; }
 DROP_REPLICATION_SLOT		{ return K_DROP_REPLICATION_SLOT; }
+ALTER_REPLICATION_SLOT		{ return K_ALTER_REPLICATION_SLOT; }
 TIMELINE_HISTORY	{ return K_TIMELINE_HISTORY; }
 PHYSICAL			{ return K_PHYSICAL; }
 RESERVE_WAL			{ return K_RESERVE_WAL; }
@@ -302,6 +303,7 @@ replication_scanner_is_replication_command(void)
 		case K_START_REPLICATION:
 		case K_CREATE_REPLICATION_SLOT:
 		case K_DROP_REPLICATION_SLOT:
+		case K_ALTER_REPLICATION_SLOT:
 		case K_READ_REPLICATION_SLOT:
 		case K_TIMELINE_HISTORY:
 		case K_UPLOAD_MANIFEST:
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 02a14ec210..f2781d0455 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -683,6 +683,31 @@ ReplicationSlotDrop(const char *name, bool nowait)
 	ReplicationSlotDropAcquired();
 }
 
+/*
+ * Change the definition of the slot identified by the specified name.
+ */
+void
+ReplicationSlotAlter(const char *name, bool failover)
+{
+	Assert(MyReplicationSlot == NULL);
+
+	ReplicationSlotAcquire(name, false);
+
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot use %s with a physical replication slot",
+					   "ALTER_REPLICATION_SLOT"));
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.failover = failover;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	ReplicationSlotRelease();
+}
+
 /*
  * Permanently drop the currently acquired replication slot.
  */
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 728059518e..e29a6196a3 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -387,7 +387,7 @@ WalReceiverMain(void)
 					 "pg_walreceiver_%lld",
 					 (long long int) walrcv_get_backend_pid(wrconn));
 
-			walrcv_create_slot(wrconn, slotname, true, false, 0, NULL);
+			walrcv_create_slot(wrconn, slotname, true, false, false, 0, NULL);
 
 			SpinLockAcquire(&walrcv->mutex);
 			strlcpy(walrcv->slotname, slotname, NAMEDATALEN);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index aa80f3de20..77c8baa32a 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1126,12 +1126,13 @@ static void
 parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 						   bool *reserve_wal,
 						   CRSSnapshotAction *snapshot_action,
-						   bool *two_phase)
+						   bool *two_phase, bool *failover)
 {
 	ListCell   *lc;
 	bool		snapshot_action_given = false;
 	bool		reserve_wal_given = false;
 	bool		two_phase_given = false;
+	bool		failover_given = false;
 
 	/* Parse options */
 	foreach(lc, cmd->options)
@@ -1181,6 +1182,15 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd,
 			two_phase_given = true;
 			*two_phase = defGetBoolean(defel);
 		}
+		else if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given || cmd->kind != REPLICATION_KIND_LOGICAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
 		else
 			elog(ERROR, "unrecognized option: %s", defel->defname);
 	}
@@ -1197,6 +1207,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	char	   *slot_name;
 	bool		reserve_wal = false;
 	bool		two_phase = false;
+	bool		failover = false;
 	CRSSnapshotAction snapshot_action = CRS_EXPORT_SNAPSHOT;
 	DestReceiver *dest;
 	TupOutputState *tstate;
@@ -1206,7 +1217,8 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	Assert(!MyReplicationSlot);
 
-	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase);
+	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
+							   &failover);
 
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
@@ -1243,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, false);
+							  two_phase, failover);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -1398,6 +1410,43 @@ DropReplicationSlot(DropReplicationSlotCmd *cmd)
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
 }
 
+/*
+ * Process extra options given to ALTER_REPLICATION_SLOT.
+ */
+static void
+ParseAlterReplSlotOptions(AlterReplicationSlotCmd *cmd, bool *failover)
+{
+	bool		failover_given = false;
+
+	/* Parse options */
+	foreach_ptr(DefElem, defel, cmd->options)
+	{
+		if (strcmp(defel->defname, "failover") == 0)
+		{
+			if (failover_given)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			failover_given = true;
+			*failover = defGetBoolean(defel);
+		}
+		else
+			elog(ERROR, "unrecognized option: %s", defel->defname);
+	}
+}
+
+/*
+ * Change the definition of a replication slot.
+ */
+static void
+AlterReplicationSlot(AlterReplicationSlotCmd *cmd)
+{
+	bool		failover = false;
+
+	ParseAlterReplSlotOptions(cmd, &failover);
+	ReplicationSlotAlter(cmd->slotname, failover);
+}
+
 /*
  * Load previously initiated logical slot and prepare for sending data (via
  * WalSndLoop).
@@ -1971,6 +2020,13 @@ exec_replication_command(const char *cmd_string)
 			EndReplicationCommand(cmdtag);
 			break;
 
+		case T_AlterReplicationSlotCmd:
+			cmdtag = "ALTER_REPLICATION_SLOT";
+			set_ps_display(cmdtag);
+			AlterReplicationSlot((AlterReplicationSlotCmd *) cmd_node);
+			EndReplicationCommand(cmdtag);
+			break;
+
 		case T_StartReplicationCmd:
 			{
 				StartReplicationCmd *cmd = (StartReplicationCmd *) cmd_node;
diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h
index af0a333f1a..ed23333e92 100644
--- a/src/include/nodes/replnodes.h
+++ b/src/include/nodes/replnodes.h
@@ -72,6 +72,18 @@ typedef struct DropReplicationSlotCmd
 } DropReplicationSlotCmd;
 
 
+/* ----------------------
+ *		ALTER_REPLICATION_SLOT command
+ * ----------------------
+ */
+typedef struct AlterReplicationSlotCmd
+{
+	NodeTag		type;
+	char	   *slotname;
+	List	   *options;
+} AlterReplicationSlotCmd;
+
+
 /* ----------------------
  *		START_REPLICATION command
  * ----------------------
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index db9bb22266..da4c776492 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -227,6 +227,7 @@ extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  bool two_phase, bool failover);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
 extern void ReplicationSlotRelease(void);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..f566a99ba1 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -355,9 +355,20 @@ typedef char *(*walrcv_create_slot_fn) (WalReceiverConn *conn,
 										const char *slotname,
 										bool temporary,
 										bool two_phase,
+										bool failover,
 										CRSSnapshotAction snapshot_action,
 										XLogRecPtr *lsn);
 
+/*
+ * walrcv_alter_slot_fn
+ *
+ * Change the definition of a replication slot. Currently, it only supports
+ * changing the failover property of the slot.
+ */
+typedef void (*walrcv_alter_slot_fn) (WalReceiverConn *conn,
+									  const char *slotname,
+									  bool failover);
+
 /*
  * walrcv_get_backend_pid_fn
  *
@@ -399,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_receive_fn walrcv_receive;
 	walrcv_send_fn walrcv_send;
 	walrcv_create_slot_fn walrcv_create_slot;
+	walrcv_alter_slot_fn walrcv_alter_slot;
 	walrcv_get_backend_pid_fn walrcv_get_backend_pid;
 	walrcv_exec_fn walrcv_exec;
 	walrcv_disconnect_fn walrcv_disconnect;
@@ -428,8 +440,10 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_receive(conn, buffer, wait_fd)
 #define walrcv_send(conn, buffer, nbytes) \
 	WalReceiverFunctions->walrcv_send(conn, buffer, nbytes)
-#define walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn) \
-	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, snapshot_action, lsn)
+#define walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn) \
+	WalReceiverFunctions->walrcv_create_slot(conn, slotname, temporary, two_phase, failover, snapshot_action, lsn)
+#define walrcv_alter_slot(conn, slotname, failover) \
+	WalReceiverFunctions->walrcv_alter_slot(conn, slotname, failover)
 #define walrcv_get_backend_pid(conn) \
 	WalReceiverFunctions->walrcv_get_backend_pid(conn)
 #define walrcv_exec(conn, exec, nRetTypes, retTypes) \
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..513c7702ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -85,6 +85,7 @@ AlterOwnerStmt
 AlterPolicyStmt
 AlterPublicationAction
 AlterPublicationStmt
+AlterReplicationSlotCmd
 AlterRoleSetStmt
 AlterRoleStmt
 AlterSeqStmt
@@ -3880,6 +3881,7 @@ varattrib_1b_e
 varattrib_4b
 vbits
 verifier_context
+walrcv_alter_slot_fn
 walrcv_check_conninfo_fn
 walrcv_connect_fn
 walrcv_create_slot_fn
-- 
2.34.1



  [application/octet-stream] v70-0002-Add-a-failover-option-to-subscriptions.patch (64.4K, ../../OS0PR01MB5716DE2A364CB23F9144AED194782@OS0PR01MB5716.jpnprd01.prod.outlook.com/6-v70-0002-Add-a-failover-option-to-subscriptions.patch)
  download | inline diff:
From d40ffc43c6ccfa1460b88756bfbffe85a3091f7a Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:51:39 +0800
Subject: [PATCH v70 2/7] Add a failover option to subscriptions.

This commit introduces a new subscription option named 'failover', which
provides users with the ability to set the failover property of the replication
slot on the publisher when creating or altering a subscription.
---
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/ref/alter_subscription.sgml      |  17 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_views.sql          |   3 +-
 src/backend/commands/subscriptioncmds.c       | 106 ++++++++++-
 src/backend/replication/logical/tablesync.c   |   2 +-
 src/backend/replication/logical/worker.c      |   7 +
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_subscription.h         |   9 +
 .../t/050_standby_failover_slots_sync.pl      |  89 +++++++++
 src/test/regress/expected/subscription.out    | 170 ++++++++++--------
 src/test/regress/sql/subscription.sql         |  14 ++
 17 files changed, 387 insertions(+), 91 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16b94461b2..880f717b10 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -8000,6 +8000,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..0c34ab9017 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,23 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription. Otherwise, the slot on the publisher may
+      not be enabled to be synced to standbys.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..59a9554bf0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index c62aa0074a..6fc3916850 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1359,7 +1359,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eaf2ec3b36..15bb10da54 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1513,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4207b9356c..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,7 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
-					   false,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd..19a3865699 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 93d97a4090..77db42e354 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -667,6 +667,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9cd8783325..b6a4eb1d56 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6571,7 +6571,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6635,6 +6636,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..4d0e364624 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -148,6 +153,10 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..f28ae12161 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,20 +412,38 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+ALTER SUBSCRIPTION regress_testsub ENABLE;
+-- fail - cannot set failover for enabled subscription
+ALTER SUBSCRIPTION regress_testsub SET (failover = false);
+ERROR:  cannot set failover for enabled subscription
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | t       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
+ALTER SUBSCRIPTION regress_testsub DISABLE;
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 -- let's do some tests with pg_create_subscription rather than superuser
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..fc03033546 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,20 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+ALTER SUBSCRIPTION regress_testsub ENABLE;
+
+-- fail - cannot set failover for enabled subscription
+ALTER SUBSCRIPTION regress_testsub SET (failover = false);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub DISABLE;
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
-- 
2.34.1



  [application/octet-stream] v70-0003-Add-logical-slot-sync-capability-to-the-physical.patch (91.4K, ../../OS0PR01MB5716DE2A364CB23F9144AED194782@OS0PR01MB5716.jpnprd01.prod.outlook.com/7-v70-0003-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 433d4687bdae7bd89992a068c31a3cf675b1706b Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Thu, 25 Jan 2024 20:28:30 +0800
Subject: [PATCH v70 3/7] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/protocol.sgml                    |    6 +-
 doc/src/sgml/system-views.sgml                |   22 +-
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1222 +++++++++++++++++
 src/backend/replication/slot.c                |   59 +-
 src/backend/replication/slotfuncs.c           |   26 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 35 files changed, 1792 insertions(+), 49 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index bb4fef1f51..f8ef2ad2ab 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2065,7 +2065,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
           The default is false.
          </para>
         </listitem>
@@ -2165,7 +2166,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
          </para>
         </listitem>
        </varlistentry>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index dd468b31ea..4ea2177b34 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2561,10 +2561,28 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        <structfield>failover</structfield> <type>bool</type>
       </para>
       <para>
-       True if this is a logical slot enabled to be synced to the standbys.
-       Always false for physical slots.
+       True if this is a logical slot enabled to be synced to the standbys
+       so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bb472da27..87b49d524a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1467,6 +1468,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6fc3916850..2e8ce9d554 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 2439733b55..61eeaa17b2 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..751d03f5b9
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1222 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/lmgr.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+	NameData	plugin_name;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	/* Avoid expensive operations while holding a spinlock. */
+	namestrcpy(&plugin_name, remote_slot->plugin);
+
+	SpinLockAcquire(&slot->mutex);
+	slot->data.plugin = plugin_name;
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			/*
+			 * Use shared lock to prevent a conflict with
+			 * ReplicationSlotsDropDBSlots(), trying to drop the same slot
+			 * while drop-database operation.
+			 */
+			LockSharedObject(DatabaseRelationId, local_slot->data.database,
+							 0, AccessShareLock);
+
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			UnlockSharedObject(DatabaseRelationId, local_slot->data.database,
+							   0, AccessShareLock);
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot, remote_dbid))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Make sure that concerned WAL is received and flushed before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check will never pass if on the primary server, user has
+	 * configured standby_slot_names GUC correctly, otherwise this can hit
+	 * frequently.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization as the received slot sync"
+					   " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+					   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+					   remote_slot->name,
+					   LSN_FORMAT_ARGS(latestFlushPtr)));
+
+		return false;
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot, remote_dbid);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot, remote_dbid))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		NameData	plugin_name;
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		/* Avoid expensive operations while holding a spinlock. */
+		namestrcpy(&plugin_name, remote_slot->plugin);
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = remote_dbid;
+		slot->data.plugin = plugin_name;
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot, remote_dbid);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info from the primary server: %s",
+					   res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+		int			col = 0;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															 &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															   &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, ++col,
+														   &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, ++col,
+														  &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 ++col, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Sanity check */
+		Assert(col == SLOTSYNC_COLUMN_COUNT);
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+	{
+		Oid			remote_dbid = get_database_oid(remote_slot->database, false);
+
+		/*
+		 * Use shared lock to prevent a conflict with
+		 * ReplicationSlotsDropDBSlots(), trying to drop the same slot while
+		 * drop-database operation.
+		 */
+		LockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+
+		some_slot_updated |= synchronize_one_slot(remote_slot, remote_dbid);
+
+		UnlockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+	}
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the primary server: %s",
+					   PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR,
+			 "failed to fetch tuple for the primary server slot specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of a parameter change"));
+
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for slotsync worker.
+ *
+ * Called on slotsync worker exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the startup process, or when there is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index f2781d0455..8caa6a9e09 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,7 +46,9 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +105,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +251,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -263,6 +265,19 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 
 	ReplicationSlotValidateName(name, ERROR);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 *
+	 * Slot sync worker can still create slots with failover enabled, as it
+	 * needs to maintain this value in sync with the remote slots.
+	 */
+	if (failover && RecoveryInProgress() && !IsLogicalSlotSyncWorker())
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " created on the standby"));
+
 	/*
 	 * If some other backend ran this code concurrently with us, we'd likely
 	 * both allocate the same slot, and that would be bad.  We'd also be at
@@ -315,6 +330,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +696,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +725,29 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter slots to enable failover on the standby
+		 * as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot enable failover for a replication slot"
+						   " on the standby"));
+	}
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +760,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +916,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index eb685089b3..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -700,7 +702,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	XLogRecPtr	src_restart_lsn;
 	bool		src_islogical;
 	bool		temporary;
-	bool		failover;
 	char	   *plugin;
 	Datum		values[2];
 	bool		nulls[2];
@@ -756,7 +757,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	src_islogical = SlotIsLogical(&first_slot_contents);
 	src_restart_lsn = first_slot_contents.data.restart_lsn;
 	temporary = (first_slot_contents.data.persistency == RS_TEMPORARY);
-	failover = first_slot_contents.data.failover;
 	plugin = logical_slot ? NameStr(first_slot_contents.data.plugin) : NULL;
 
 	/* Check type of replication slot */
@@ -791,12 +791,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
-										failover,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..f753aed345 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..994e33f59b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11127,9 +11127,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index da4c776492..1f4446aa3a 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for standbys)? Only relevant
 	 * for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..1055573cde 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t",
+	'logical slot has synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index abc944e8b8..b7488d760e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 513c7702ff..d527bf918b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2585,6 +2586,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v70-0005-Allow-logical-walsenders-to-wait-for-the-physica.patch (41.2K, ../../OS0PR01MB5716DE2A364CB23F9144AED194782@OS0PR01MB5716.jpnprd01.prod.outlook.com/8-v70-0005-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 9b5eb655766cf7577446bb5e69c9a14d7865accb Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 14:28:42 +0530
Subject: [PATCH v70 5/7] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/logical/slotsync.c    |   1 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 17 files changed, 739 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 94ca93f886..68ad8bbcbc 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -44,6 +44,7 @@
 #include "commands/dbcommands.h"
 #include "libpq/pqsignal.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "postmaster/bgworker.h"
 #include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 8caa6a9e09..8dc2bc7c95 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,14 +46,19 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -100,10 +105,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2237,3 +2251,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index f753aed345..71933f4bf1 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 1f4446aa3a..1054910cac 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 1055573cde..06a4ada941 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 03:50  Peter Smith <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Peter Smith @ 2024-01-29 03:50 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Here are some review comments for v70-0001.

======
doc/src/sgml/protocol.sgml

1.
Related to this, please also review my other patch to the same docs
page protocol.sgml [1].

======
src/backend/replication/logical/tablesync.c

2.
  walrcv_create_slot(LogRepWorkerWalRcvConn,
     slotname, false /* permanent */ , false /* two_phase */ ,
+    false,
     CRS_USE_SNAPSHOT, origin_startpos);

I know it was previously mentioned in this thread that inline
parameter comments are unnecessary, but here they are already in the
existing code so shouldn't we do the same?

======
src/backend/replication/repl_gram.y

3.
+/* ALTER_REPLICATION_SLOT slot options */
+alter_replication_slot:
+ K_ALTER_REPLICATION_SLOT IDENT '(' generic_option_list ')'
+ {
+ AlterReplicationSlotCmd *cmd;
+ cmd = makeNode(AlterReplicationSlotCmd);
+ cmd->slotname = $2;
+ cmd->options = $4;
+ $$ = (Node *) cmd;
+ }
+ ;
+

IMO write that comment with parentheses, so it matches the code.

SUGGESTION
ALTER_REPLICATION_SLOT slot ( options )

======
[1] https://www.postgresql.org/message-id/CAHut%2BPtDWSmW8uiRJF1LfGQJikmo7V2jdysLuRmtsanNZc7fNw%40mail.g...

Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 04:05  Amit Kapila <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-29 04:05 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Bertrand Drouvot <[email protected]>; shveta malik <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Mon, Jan 29, 2024 at 9:21 AM Peter Smith <[email protected]> wrote:
>
> Here are some review comments for v70-0001.
>
> ======
> doc/src/sgml/protocol.sgml
>
> 1.
> Related to this, please also review my other patch to the same docs
> page protocol.sgml [1].
>

We can check that separately.

> ======
> src/backend/replication/logical/tablesync.c
>
> 2.
>   walrcv_create_slot(LogRepWorkerWalRcvConn,
>      slotname, false /* permanent */ , false /* two_phase */ ,
> +    false,
>      CRS_USE_SNAPSHOT, origin_startpos);
>
> I know it was previously mentioned in this thread that inline
> parameter comments are unnecessary, but here they are already in the
> existing code so shouldn't we do the same?
>

I think it is better to remove the even existing ones as those many
times make code difficult to read.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 04:54  shveta malik <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-29 04:54 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Sat, Jan 27, 2024 at 12:02 PM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> Attach the V70 patch set which addressed above comments and Bertrand's comments in [1]
>

Since v70-0001 is pushed, rebased and attached v70_2 patches.  There
are no new changes.

thanks
Shveta


Attachments:

  [application/octet-stream] v70_2-0002-Add-logical-slot-sync-capability-to-the-physic.patch (91.4K, ../../CAJpy0uBX2tyF4bQn-rd8zqoVLr+j53uWp+fh3qbLfXwCnKed4A@mail.gmail.com/2-v70_2-0002-Add-logical-slot-sync-capability-to-the-physic.patch)
  download | inline diff:
From 1c43185ead1c22f107801a1aab93307f49eebb06 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Thu, 25 Jan 2024 20:28:30 +0800
Subject: [PATCH v70_2 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/protocol.sgml                    |    6 +-
 doc/src/sgml/system-views.sgml                |   22 +-
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1222 +++++++++++++++++
 src/backend/replication/slot.c                |   59 +-
 src/backend/replication/slotfuncs.c           |   26 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 35 files changed, 1792 insertions(+), 49 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index bb4fef1f51..f8ef2ad2ab 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2065,7 +2065,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
           The default is false.
          </para>
         </listitem>
@@ -2165,7 +2166,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
          </para>
         </listitem>
        </varlistentry>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index dd468b31ea..4ea2177b34 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2561,10 +2561,28 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        <structfield>failover</structfield> <type>bool</type>
       </para>
       <para>
-       True if this is a logical slot enabled to be synced to the standbys.
-       Always false for physical slots.
+       True if this is a logical slot enabled to be synced to the standbys
+       so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bb472da27..87b49d524a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1467,6 +1468,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6fc3916850..2e8ce9d554 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 2439733b55..61eeaa17b2 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..751d03f5b9
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1222 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/lmgr.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+	NameData	plugin_name;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	/* Avoid expensive operations while holding a spinlock. */
+	namestrcpy(&plugin_name, remote_slot->plugin);
+
+	SpinLockAcquire(&slot->mutex);
+	slot->data.plugin = plugin_name;
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			/*
+			 * Use shared lock to prevent a conflict with
+			 * ReplicationSlotsDropDBSlots(), trying to drop the same slot
+			 * while drop-database operation.
+			 */
+			LockSharedObject(DatabaseRelationId, local_slot->data.database,
+							 0, AccessShareLock);
+
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			UnlockSharedObject(DatabaseRelationId, local_slot->data.database,
+							   0, AccessShareLock);
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot, remote_dbid))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Make sure that concerned WAL is received and flushed before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check will never pass if on the primary server, user has
+	 * configured standby_slot_names GUC correctly, otherwise this can hit
+	 * frequently.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization as the received slot sync"
+					   " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+					   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+					   remote_slot->name,
+					   LSN_FORMAT_ARGS(latestFlushPtr)));
+
+		return false;
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot, remote_dbid);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot, remote_dbid))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		NameData	plugin_name;
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true /* synced */ );
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		/* Avoid expensive operations while holding a spinlock. */
+		namestrcpy(&plugin_name, remote_slot->plugin);
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = remote_dbid;
+		slot->data.plugin = plugin_name;
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot, remote_dbid);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info from the primary server: %s",
+					   res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+		int			col = 0;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															 &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															   &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, ++col,
+														   &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, ++col,
+														  &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 ++col, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Sanity check */
+		Assert(col == SLOTSYNC_COLUMN_COUNT);
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+	{
+		Oid			remote_dbid = get_database_oid(remote_slot->database, false);
+
+		/*
+		 * Use shared lock to prevent a conflict with
+		 * ReplicationSlotsDropDBSlots(), trying to drop the same slot while
+		 * drop-database operation.
+		 */
+		LockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+
+		some_slot_updated |= synchronize_one_slot(remote_slot, remote_dbid);
+
+		UnlockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+	}
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the primary server: %s",
+					   PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR,
+			 "failed to fetch tuple for the primary server slot specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of a parameter change"));
+
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for slotsync worker.
+ *
+ * Called on slotsync worker exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the startup process, or when there is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index f2781d0455..8caa6a9e09 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,7 +46,9 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +105,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +251,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -263,6 +265,19 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 
 	ReplicationSlotValidateName(name, ERROR);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 *
+	 * Slot sync worker can still create slots with failover enabled, as it
+	 * needs to maintain this value in sync with the remote slots.
+	 */
+	if (failover && RecoveryInProgress() && !IsLogicalSlotSyncWorker())
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " created on the standby"));
+
 	/*
 	 * If some other backend ran this code concurrently with us, we'd likely
 	 * both allocate the same slot, and that would be bad.  We'd also be at
@@ -315,6 +330,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -680,6 +696,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -699,6 +725,29 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter slots to enable failover on the standby
+		 * as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot enable failover for a replication slot"
+						   " on the standby"));
+	}
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -711,7 +760,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -867,8 +916,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index eb685089b3..843ae8cd68 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false /* synced */ );
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false /* synced */ );
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -700,7 +702,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	XLogRecPtr	src_restart_lsn;
 	bool		src_islogical;
 	bool		temporary;
-	bool		failover;
 	char	   *plugin;
 	Datum		values[2];
 	bool		nulls[2];
@@ -756,7 +757,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	src_islogical = SlotIsLogical(&first_slot_contents);
 	src_restart_lsn = first_slot_contents.data.restart_lsn;
 	temporary = (first_slot_contents.data.persistency == RS_TEMPORARY);
-	failover = first_slot_contents.data.failover;
 	plugin = logical_slot ? NameStr(first_slot_contents.data.plugin) : NULL;
 
 	/* Check type of replication slot */
@@ -791,12 +791,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
-										failover,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..f753aed345 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false /* synced */ );
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false /* synced */ );
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..994e33f59b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11127,9 +11127,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index da4c776492..1f4446aa3a 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for standbys)? Only relevant
 	 * for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..1055573cde 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t",
+	'logical slot has synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index abc944e8b8..b7488d760e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90b37b919c..62554d527e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2584,6 +2585,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v70_2-0001-Add-a-failover-option-to-subscriptions.patch (64.4K, ../../CAJpy0uBX2tyF4bQn-rd8zqoVLr+j53uWp+fh3qbLfXwCnKed4A@mail.gmail.com/3-v70_2-0001-Add-a-failover-option-to-subscriptions.patch)
  download | inline diff:
From 3dd209d292a13a437052bb1268242187e606a2ae Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:51:39 +0800
Subject: [PATCH v70_2 1/6] Add a failover option to subscriptions.

This commit introduces a new subscription option named 'failover', which
provides users with the ability to set the failover property of the replication
slot on the publisher when creating or altering a subscription.
---
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/ref/alter_subscription.sgml      |  17 +-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_views.sql          |   3 +-
 src/backend/commands/subscriptioncmds.c       | 106 ++++++++++-
 src/backend/replication/logical/tablesync.c   |   2 +-
 src/backend/replication/logical/worker.c      |   7 +
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_subscription.h         |   9 +
 .../t/050_standby_failover_slots_sync.pl      |  89 +++++++++
 src/test/regress/expected/subscription.out    | 170 ++++++++++--------
 src/test/regress/sql/subscription.sql         |  14 ++
 17 files changed, 387 insertions(+), 91 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16b94461b2..880f717b10 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -8000,6 +8000,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..0c34ab9017 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,23 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription. Otherwise, the slot on the publisher may
+      not be enabled to be synced to standbys.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..59a9554bf0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index c62aa0074a..6fc3916850 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1359,7 +1359,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eaf2ec3b36..15bb10da54 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1513,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4207b9356c..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,7 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
-					   false,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd..19a3865699 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 93d97a4090..77db42e354 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -667,6 +667,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9cd8783325..b6a4eb1d56 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6571,7 +6571,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6635,6 +6636,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..4d0e364624 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -148,6 +153,10 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..f28ae12161 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,20 +412,38 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+ALTER SUBSCRIPTION regress_testsub ENABLE;
+-- fail - cannot set failover for enabled subscription
+ALTER SUBSCRIPTION regress_testsub SET (failover = false);
+ERROR:  cannot set failover for enabled subscription
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | t       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
+ALTER SUBSCRIPTION regress_testsub DISABLE;
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 -- let's do some tests with pg_create_subscription rather than superuser
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..fc03033546 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,20 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+ALTER SUBSCRIPTION regress_testsub ENABLE;
+
+-- fail - cannot set failover for enabled subscription
+ALTER SUBSCRIPTION regress_testsub SET (failover = false);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub DISABLE;
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
-- 
2.34.1



  [application/octet-stream] v70_2-0004-Allow-logical-walsenders-to-wait-for-the-physi.patch (41.2K, ../../CAJpy0uBX2tyF4bQn-rd8zqoVLr+j53uWp+fh3qbLfXwCnKed4A@mail.gmail.com/4-v70_2-0004-Allow-logical-walsenders-to-wait-for-the-physi.patch)
  download | inline diff:
From 504a3bc5588a8d9032ff66b48ea2afd784a3dd25 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 14:28:42 +0530
Subject: [PATCH v70_2 4/6] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/logical/slotsync.c    |   1 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 17 files changed, 739 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 94ca93f886..68ad8bbcbc 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -44,6 +44,7 @@
 #include "commands/dbcommands.h"
 #include "libpq/pqsignal.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "postmaster/bgworker.h"
 #include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 8caa6a9e09..8dc2bc7c95 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,14 +46,19 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -100,10 +105,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2237,3 +2251,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 843ae8cd68..0a09b6c508 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index f753aed345..71933f4bf1 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 1f4446aa3a..1054910cac 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 1055573cde..06a4ada941 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v70_2-0003-Slot-sync-worker-as-a-special-process.patch (38.4K, ../../CAJpy0uBX2tyF4bQn-rd8zqoVLr+j53uWp+fh3qbLfXwCnKed4A@mail.gmail.com/5-v70_2-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From e51bf5a248b57392e782b4a7ab98a3aef26a0be5 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 12:54:09 +0530
Subject: [PATCH v70_2 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  86 +++--
 src/backend/replication/logical/slotsync.c    | 359 ++++++++++++++----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   8 +-
 14 files changed, 387 insertions(+), 181 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..adfaf75cf9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,10 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY && \
+	 SlotSyncWorkerCanRestart())
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1016,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1836,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2687,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3046,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3218,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3600,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3746,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3763,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3779,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3878,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4102,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4916,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5020,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5849,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 751d03f5b9..94ca93f886 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -34,15 +34,20 @@
 
 #include "postgres.h"
 
+#include <time.h>
+
 #include "access/genam.h"
 #include "access/table.h"
 #include "access/xlog_internal.h"
 #include "access/xlogrecovery.h"
 #include "catalog/pg_database.h"
 #include "commands/dbcommands.h"
+#include "libpq/pqsignal.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -56,6 +61,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -109,8 +116,16 @@ bool		enable_syncslot = false;
 #define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -734,7 +749,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -749,8 +765,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -788,13 +806,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -803,20 +824,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -824,11 +840,14 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -836,49 +855,98 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
-		/* translator: %s is a GUC variable name */
+	{
+		ereport(LOG,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -894,16 +962,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -911,8 +991,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of a parameter change"));
 
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -929,7 +1008,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
 		proc_exit(0);
@@ -961,17 +1041,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -1007,20 +1086,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -1035,26 +1132,77 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	/*
+	 * Unblock signals (they were blocked when the postmaster forked us)
+	 */
+	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1073,26 +1221,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1108,7 +1259,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1138,7 +1289,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1181,42 +1332,92 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * SlotSyncWorkerCanRestart
+ *
+ * Returns true if the worker is allowed to restart if enough time has
+ * passed (SLOTSYNC_RESTART_INTERVAL_SEC) since it was launched last.
+ * Otherwise returns false.
+ *
+ * This is a safety valve to protect against continuous respawn attempts if the
+ * worker is dying immediately at launch. Note that since we will retry to
+ * launch the worker from the postmaster main loop, we will get another
+ * chance later.
+ */
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+	static time_t last_slotsync_start_time = 0;
+	time_t		curtime = time(NULL);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+	/* Return false if too soon since last start. */
+	if ((unsigned int) (curtime - last_slotsync_start_time) <
+		(unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC)
+		return false;
+
+	last_slotsync_start_time = curtime;
+	return true;
+}
+
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
+
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
+
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe..f19e5faeaf 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..aa01f63648 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,11 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
+extern bool SlotSyncWorkerCanRestart(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v70_2-0005-Non-replication-connection-and-app_name-change.patch (9.7K, ../../CAJpy0uBX2tyF4bQn-rd8zqoVLr+j53uWp+fh3qbLfXwCnKed4A@mail.gmail.com/6-v70_2-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 890dded8ad4e9061b2231ae6d56c828c6e0198ff Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v70_2 5/6] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       | 12 +++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 14 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  4 +-
 src/backend/replication/walreceiver.c         |  3 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 15bb10da54..f17c666ebc 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true /* replication */ ,
+								true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +905,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+							true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1531,7 +1533,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true /* replication */ ,
+								true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1782,7 +1785,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 61eeaa17b2..512ec2897a 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 68ad8bbcbc..9afc2a0381 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1096,6 +1096,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1205,13 +1206,22 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false /* replication */,
+							false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..c5c6ac4bac 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true /* replication */ , true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..0d791c188c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4518,7 +4518,9 @@ run_apply_worker()
 	must_use_password = MySubscription->passwordrequired &&
 		!MySubscription->ownersuperuser;
 
-	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
+	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo,
+											true /* replication */ ,
+											true,
 											must_use_password,
 											MySubscription->name, &err);
 
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..872cccb54b 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,8 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true /* replication */ ,
+							false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v70_2-0006-Document-the-steps-to-check-if-the-standby-is-.patch (6.6K, ../../CAJpy0uBX2tyF4bQn-rd8zqoVLr+j53uWp+fh3qbLfXwCnKed4A@mail.gmail.com/7-v70_2-0006-Document-the-steps-to-check-if-the-standby-is-.patch)
  download | inline diff:
From b5b47bcd7e10f501d79f94e1472131217cdebec7 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v70_2 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 08:52  Bertrand Drouvot <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-29 08:52 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Hi,

On Mon, Jan 29, 2024 at 10:24:11AM +0530, shveta malik wrote:
> On Sat, Jan 27, 2024 at 12:02 PM Zhijie Hou (Fujitsu)
> <[email protected]> wrote:
> >
> > Attach the V70 patch set which addressed above comments and Bertrand's comments in [1]
> >
> 
> Since v70-0001 is pushed, rebased and attached v70_2 patches.  There
> are no new changes.

Thanks!

Looking at 0001:

+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription. Otherwise, the slot on the publisher may
+      not be enabled to be synced to standbys.

Not related to this patch series but while at it shouldn't we also add a few
words about two_phase too? (I mean ensure the slot property matchs the
subscription one).

Or would it be better to create a dedicated patch (outside of this thread) for
the "two_phase" remark? (If so I can take care of it).

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 09:05  Amit Kapila <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-29 09:05 UTC (permalink / raw)
  To: Bertrand Drouvot <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Mon, Jan 29, 2024 at 2:22 PM Bertrand Drouvot
<[email protected]> wrote:
>
> On Mon, Jan 29, 2024 at 10:24:11AM +0530, shveta malik wrote:
> > On Sat, Jan 27, 2024 at 12:02 PM Zhijie Hou (Fujitsu)
> > <[email protected]> wrote:
> > >
> > > Attach the V70 patch set which addressed above comments and Bertrand's comments in [1]
> > >
> >
> > Since v70-0001 is pushed, rebased and attached v70_2 patches.  There
> > are no new changes.
>
> Thanks!
>
> Looking at 0001:
>
> +      When altering the
> +      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
> +      the <literal>failover</literal> property value of the named slot may differ from the
> +      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
> +      parameter specified in the subscription. When creating the slot,
> +      ensure the slot <literal>failover</literal> property matches the
> +      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
> +      parameter value of the subscription. Otherwise, the slot on the publisher may
> +      not be enabled to be synced to standbys.
>
> Not related to this patch series but while at it shouldn't we also add a few
> words about two_phase too? (I mean ensure the slot property matchs the
> subscription one).
>
> Or would it be better to create a dedicated patch (outside of this thread) for
> the "two_phase" remark? (If so I can take care of it).
>

I think it is better to create a separate patch for two_phase after
this patch gets committed.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 09:15  Bertrand Drouvot <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-29 09:15 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Hi,

On Mon, Jan 29, 2024 at 02:35:52PM +0530, Amit Kapila wrote:
> On Mon, Jan 29, 2024 at 2:22 PM Bertrand Drouvot
> <[email protected]> wrote:
> > Looking at 0001:
> >
> > +      When altering the
> > +      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
> > +      the <literal>failover</literal> property value of the named slot may differ from the
> > +      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
> > +      parameter specified in the subscription. When creating the slot,
> > +      ensure the slot <literal>failover</literal> property matches the
> > +      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
> > +      parameter value of the subscription. Otherwise, the slot on the publisher may
> > +      not be enabled to be synced to standbys.
> >
> > Not related to this patch series but while at it shouldn't we also add a few
> > words about two_phase too? (I mean ensure the slot property matchs the
> > subscription one).
> >
> > Or would it be better to create a dedicated patch (outside of this thread) for
> > the "two_phase" remark? (If so I can take care of it).
> >
> 
> I think it is better to create a separate patch for two_phase after
> this patch gets committed.

Yeah, makes sense, will do, thanks!

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 09:41  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-29 09:41 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Peter Smith <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Mon, Jan 29, 2024 at 9:35 AM Amit Kapila <[email protected]> wrote:
>
> >
> > 2.
> >   walrcv_create_slot(LogRepWorkerWalRcvConn,
> >      slotname, false /* permanent */ , false /* two_phase */ ,
> > +    false,
> >      CRS_USE_SNAPSHOT, origin_startpos);
> >
> > I know it was previously mentioned in this thread that inline
> > parameter comments are unnecessary, but here they are already in the
> > existing code so shouldn't we do the same?
> >
>
> I think it is better to remove the even existing ones as those many
> times make code difficult to read.

I had earlier added inline comments in callers of
ReplicationSlotCreate() and walrcv_connect() for new args 'synced' and
'replication' respectively, removing those changes from pacth002 and
patch005 now.
Also improved alter-sub doc in patch001 as suggested by Peter offlist.

PFA v71 patch set with above changes.

thanks
Shveta


Attachments:

  [application/octet-stream] v71-0005-Non-replication-connection-and-app_name-change.patch (9.4K, ../../CAJpy0uAv2dS+NTw1X1uZep11WM24Wog2kN1oqJu4Zx4xjpgwig@mail.gmail.com/2-v71-0005-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 0aad60a608222330398887ceba3f85361fdfc20e Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v71 5/6] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       |  8 ++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 13 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  2 +-
 src/backend/replication/walreceiver.c         |  2 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 52 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 15bb10da54..98a9fc644c 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -753,7 +753,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true,	true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -904,7 +904,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1531,7 +1531,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1782,7 +1782,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true,	true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 61eeaa17b2..512ec2897a 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 0dfc057ff9..c6f02dae8d 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1096,6 +1096,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1205,13 +1206,21 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false, false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..ee06629088 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true, true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..9dd2446fbf 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4519,7 +4519,7 @@ run_apply_worker()
 		!MySubscription->ownersuperuser;
 
 	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
-											must_use_password,
+											true, must_use_password,
 											MySubscription->name, &err);
 
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..b80447d15f 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,7 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true, false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v71-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch (41.2K, ../../CAJpy0uAv2dS+NTw1X1uZep11WM24Wog2kN1oqJu4Zx4xjpgwig@mail.gmail.com/3-v71-0004-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 561a4acc760e918147facf9b10f863fdffbb0366 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 14:28:42 +0530
Subject: [PATCH v71 4/6] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   9 +-
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/logical/slotsync.c    |   1 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/meson.build                 |   1 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/050_standby_failover_slots_sync.pl      | 232 ++++++++++--
 17 files changed, 739 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bd2d2f871e..76345e433c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ec14cf7325..965ee716e2 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -372,7 +372,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      to work, it is mandatory to have a physical replication slot between the
      primary and the standby, and
      <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
-     must be enabled on the standby.
+     must be enabled on the standby. It's also highly recommended that the said
+     physical replication slot is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 76a73aa680..0dfc057ff9 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -44,6 +44,7 @@
 #include "commands/dbcommands.h"
 #include "libpq/pqsignal.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "postmaster/bgworker.h"
 #include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 605dfb0426..148b590641 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,14 +46,19 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -100,10 +105,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2234,3 +2248,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool	inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names has
+		 * been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 1bf639223e..6227d14158 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 15f045fe1f..7f859748c0 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms the
+	 * receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 1f4446aa3a..1054910cac 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+									 List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..4152c07318 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/050_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 1055573cde..06a4ada941 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -87,17 +87,30 @@ is( $publisher->safe_psql(
 $subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -107,21 +120,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
 
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+my $result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -150,18 +343,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
@@ -199,7 +385,9 @@ $standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;')
 $standby1->restart;
 
 # Attempting to perform logical decoding on a synced slot should result in an error
-my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+my ($stdout, $stderr);
+
+($result, $stdout, $stderr) = $standby1->psql('postgres',
 	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
 ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
 	"logical decoding is not allowed on synced slot");
-- 
2.34.1



  [application/octet-stream] v71-0003-Slot-sync-worker-as-a-special-process.patch (38.4K, ../../CAJpy0uAv2dS+NTw1X1uZep11WM24Wog2kN1oqJu4Zx4xjpgwig@mail.gmail.com/4-v71-0003-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From 21b563c878d5cd0ee8f102566c41ae31b9555516 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Thu, 25 Jan 2024 12:54:09 +0530
Subject: [PATCH v71 3/6] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  86 +++--
 src/backend/replication/logical/slotsync.c    | 359 ++++++++++++++----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   8 +-
 14 files changed, 387 insertions(+), 181 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d90d5d1576..adfaf75cf9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,10 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY && \
+	 SlotSyncWorkerCanRestart())
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1016,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1836,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2687,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3046,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3218,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3600,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3746,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3763,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3779,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3878,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4102,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4916,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5020,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5849,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-					 pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index e44474dd58..76a73aa680 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -34,15 +34,20 @@
 
 #include "postgres.h"
 
+#include <time.h>
+
 #include "access/genam.h"
 #include "access/table.h"
 #include "access/xlog_internal.h"
 #include "access/xlogrecovery.h"
 #include "catalog/pg_database.h"
 #include "commands/dbcommands.h"
+#include "libpq/pqsignal.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -56,6 +61,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -109,8 +116,16 @@ bool		enable_syncslot = false;
 #define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -734,7 +749,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -749,8 +765,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -788,13 +806,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -803,20 +824,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -824,11 +840,14 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -836,49 +855,98 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
-		/* translator: %s is a GUC variable name */
+	{
+		ereport(LOG,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -894,16 +962,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -911,8 +991,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of a parameter change"));
 
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -929,7 +1008,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
 		proc_exit(0);
@@ -961,17 +1041,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -1007,20 +1086,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -1035,26 +1132,77 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	/*
+	 * Unblock signals (they were blocked when the postmaster forked us)
+	 */
+	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1073,26 +1221,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1108,7 +1259,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1138,7 +1289,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1181,42 +1332,92 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * SlotSyncWorkerCanRestart
+ *
+ * Returns true if the worker is allowed to restart if enough time has
+ * passed (SLOTSYNC_RESTART_INTERVAL_SEC) since it was launched last.
+ * Otherwise returns false.
+ *
+ * This is a safety valve to protect against continuous respawn attempts if the
+ * worker is dying immediately at launch. Note that since we will retry to
+ * launch the worker from the postmaster main loop, we will get another
+ * chance later.
+ */
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+	static time_t last_slotsync_start_time = 0;
+	time_t		curtime = time(NULL);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+	/* Return false if too soon since last start. */
+	if ((unsigned int) (curtime - last_slotsync_start_time) <
+		(unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC)
+		return false;
+
+	last_slotsync_start_time = curtime;
+	return true;
+}
+
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
+
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
+
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe..f19e5faeaf 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..aa01f63648 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,11 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int StartSlotSyncWorker(void);
+extern bool SlotSyncWorkerCanRestart(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v71-0001-Add-a-failover-option-to-subscriptions.patch (64.9K, ../../CAJpy0uAv2dS+NTw1X1uZep11WM24Wog2kN1oqJu4Zx4xjpgwig@mail.gmail.com/5-v71-0001-Add-a-failover-option-to-subscriptions.patch)
  download | inline diff:
From ab9fe789ba2950fb0a9d96f1db16bc6596c682d1 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:51:39 +0800
Subject: [PATCH v71 1/6] Add a failover option to subscriptions.

This commit introduces a new subscription option named 'failover', which
provides users with the ability to set the failover property of the replication
slot on the publisher when creating or altering a subscription.
---
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/ref/alter_subscription.sgml      |  25 ++-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_views.sql          |   3 +-
 src/backend/commands/subscriptioncmds.c       | 106 ++++++++++-
 src/backend/replication/logical/tablesync.c   |   2 +-
 src/backend/replication/logical/worker.c      |   7 +
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_subscription.h         |   9 +
 .../t/050_standby_failover_slots_sync.pl      |  89 +++++++++
 src/test/regress/expected/subscription.out    | 170 ++++++++++--------
 src/test/regress/sql/subscription.sql         |  14 ++
 17 files changed, 395 insertions(+), 91 deletions(-)
 create mode 100644 src/test/recovery/t/050_standby_failover_slots_sync.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16b94461b2..880f717b10 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -8000,6 +8000,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..e9e6d9d74a 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,31 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription. Otherwise, the slot on the
+      publisher may behave differently from what subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option says. The slot on the publisher could either be
+      synced to the standbys even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is disabled or could be disabled for sync
+      even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is enabled.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..59a9554bf0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index c62aa0074a..6fc3916850 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1359,7 +1359,8 @@ REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
 			  subpasswordrequired, subrunasowner,
-              subslotname, subsynccommit, subpublications, suborigin)
+              subslotname, subsynccommit, subpublications, suborigin,
+              subfailover)
     ON pg_subscription TO public;
 
 CREATE VIEW pg_stat_subscription_stats AS
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eaf2ec3b36..15bb10da54 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -591,7 +604,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +724,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +823,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +832,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1166,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1253,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1513,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4207b9356c..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,7 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
-					   false,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd..19a3865699 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 93d97a4090..77db42e354 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -667,6 +667,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9cd8783325..b6a4eb1d56 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6571,7 +6571,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6635,6 +6636,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..4d0e364624 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -148,6 +153,10 @@ typedef struct Subscription
 	List	   *publications;	/* List of publication names to subscribe to */
 	char	   *origin;			/* Only publish data originating from the
 								 * specified origin */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 } Subscription;
 
 /* Disallow streaming in-progress transactions. */
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..646293c39e
--- /dev/null
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -0,0 +1,89 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+
+done_testing();
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..f28ae12161 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -116,18 +116,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +145,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +157,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +176,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +188,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +223,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +255,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +279,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +314,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +332,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +371,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +383,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +396,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,20 +412,38 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
+(1 row)
+
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+WARNING:  subscription was created, but is not connected
+HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
+ALTER SUBSCRIPTION regress_testsub ENABLE;
+-- fail - cannot set failover for enabled subscription
+ALTER SUBSCRIPTION regress_testsub SET (failover = false);
+ERROR:  cannot set failover for enabled subscription
+\dRs+
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | t       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | t        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
+ALTER SUBSCRIPTION regress_testsub DISABLE;
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 -- let's do some tests with pg_create_subscription rather than superuser
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..fc03033546 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -290,6 +290,20 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 DROP SUBSCRIPTION regress_testsub;
 
+-- test failover option
+CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+
+ALTER SUBSCRIPTION regress_testsub ENABLE;
+
+-- fail - cannot set failover for enabled subscription
+ALTER SUBSCRIPTION regress_testsub SET (failover = false);
+
+\dRs+
+
+ALTER SUBSCRIPTION regress_testsub DISABLE;
+ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
+DROP SUBSCRIPTION regress_testsub;
+
 -- let's do some tests with pg_create_subscription rather than superuser
 SET SESSION AUTHORIZATION regress_subscription_user3;
 
-- 
2.34.1



  [application/octet-stream] v71-0002-Add-logical-slot-sync-capability-to-the-physical.patch (91.3K, ../../CAJpy0uAv2dS+NTw1X1uZep11WM24Wog2kN1oqJu4Zx4xjpgwig@mail.gmail.com/6-v71-0002-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 67ae5fc98b819dd017f98506440a7f417b60fb0a Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Thu, 25 Jan 2024 20:28:30 +0800
Subject: [PATCH v71 2/6] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   37 +
 doc/src/sgml/protocol.sgml                    |    6 +-
 doc/src/sgml/system-views.sgml                |   22 +-
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1222 +++++++++++++++++
 src/backend/replication/slot.c                |   59 +-
 src/backend/replication/slotfuncs.c           |   26 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/050_standby_failover_slots_sync.pl      |  166 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 35 files changed, 1792 insertions(+), 49 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..bd2d2f871e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="guc-enable-syncslot"/>) then it is also
+          necessary to specify <literal>dbname</literal> in the
+          <varname>primary_conninfo</varname> string. This will only be used for
+          slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..ec14cf7325 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,43 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index bb4fef1f51..f8ef2ad2ab 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2065,7 +2065,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
           The default is false.
          </para>
         </listitem>
@@ -2165,7 +2166,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
          </para>
         </listitem>
        </varlistentry>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index dd468b31ea..4ea2177b34 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2561,10 +2561,28 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        <structfield>failover</structfield> <type>bool</type>
       </para>
       <para>
-       True if this is a logical slot enabled to be synced to the standbys.
-       Always false for physical slots.
+       True if this is a logical slot enabled to be synced to the standbys
+       so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bb472da27..87b49d524a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1467,6 +1468,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6fc3916850..2e8ce9d554 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..d90d5d1576 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+					 pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 2439733b55..61eeaa17b2 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..e44474dd58
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1222 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/lmgr.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+	NameData	plugin_name;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	/* Avoid expensive operations while holding a spinlock. */
+	namestrcpy(&plugin_name, remote_slot->plugin);
+
+	SpinLockAcquire(&slot->mutex);
+	slot->data.plugin = plugin_name;
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			/*
+			 * Use shared lock to prevent a conflict with
+			 * ReplicationSlotsDropDBSlots(), trying to drop the same slot
+			 * while drop-database operation.
+			 */
+			LockSharedObject(DatabaseRelationId, local_slot->data.database,
+							 0, AccessShareLock);
+
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			UnlockSharedObject(DatabaseRelationId, local_slot->data.database,
+							   0, AccessShareLock);
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot, remote_dbid))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Make sure that concerned WAL is received and flushed before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check will never pass if on the primary server, user has
+	 * configured standby_slot_names GUC correctly, otherwise this can hit
+	 * frequently.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization as the received slot sync"
+					   " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+					   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+					   remote_slot->name,
+					   LSN_FORMAT_ARGS(latestFlushPtr)));
+
+		return false;
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot, remote_dbid);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot, remote_dbid))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		NameData	plugin_name;
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true);
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		/* Avoid expensive operations while holding a spinlock. */
+		namestrcpy(&plugin_name, remote_slot->plugin);
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = remote_dbid;
+		slot->data.plugin = plugin_name;
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot, remote_dbid);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info from the primary server: %s",
+					   res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+		int			col = 0;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															 &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															   &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, ++col,
+														   &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, ++col,
+														  &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 ++col, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Sanity check */
+		Assert(col == SLOTSYNC_COLUMN_COUNT);
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+	{
+		Oid			remote_dbid = get_database_oid(remote_slot->database, false);
+
+		/*
+		 * Use shared lock to prevent a conflict with
+		 * ReplicationSlotsDropDBSlots(), trying to drop the same slot while
+		 * drop-database operation.
+		 */
+		LockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+
+		some_slot_updated |= synchronize_one_slot(remote_slot, remote_dbid);
+
+		UnlockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+	}
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the primary server: %s",
+					   PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR,
+			 "failed to fetch tuple for the primary server slot specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of a parameter change"));
+
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for slotsync worker.
+ *
+ * Called on slotsync worker exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the startup process, or when there is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 110cb59783..605dfb0426 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,7 +46,9 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +105,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +251,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -263,6 +265,19 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 
 	ReplicationSlotValidateName(name, ERROR);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 *
+	 * Slot sync worker can still create slots with failover enabled, as it
+	 * needs to maintain this value in sync with the remote slots.
+	 */
+	if (failover && RecoveryInProgress() && !IsLogicalSlotSyncWorker())
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " created on the standby"));
+
 	/*
 	 * If some other backend ran this code concurrently with us, we'd likely
 	 * both allocate the same slot, and that would be bad.  We'd also be at
@@ -315,6 +330,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -677,6 +693,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -696,6 +722,29 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter slots to enable failover on the standby
+		 * as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot enable failover for a replication slot"
+						   " on the standby"));
+	}
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -708,7 +757,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -864,8 +913,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index eb685089b3..1bf639223e 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false);
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -700,7 +702,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	XLogRecPtr	src_restart_lsn;
 	bool		src_islogical;
 	bool		temporary;
-	bool		failover;
 	char	   *plugin;
 	Datum		values[2];
 	bool		nulls[2];
@@ -756,7 +757,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	src_islogical = SlotIsLogical(&first_slot_contents);
 	src_restart_lsn = first_slot_contents.data.restart_lsn;
 	temporary = (first_slot_contents.data.persistency == RS_TEMPORARY);
-	failover = first_slot_contents.data.failover;
 	plugin = logical_slot ? NameStr(first_slot_contents.data.plugin) : NULL;
 
 	/* Check type of replication slot */
@@ -791,12 +791,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the restart_lsn
+		 * of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
-										failover,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..15f045fe1f 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false);
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..994e33f59b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11127,9 +11127,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index da4c776492..1f4446aa3a 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for standbys)? Only relevant
 	 * for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/050_standby_failover_slots_sync.pl b/src/test/recovery/t/050_standby_failover_slots_sync.pl
index 646293c39e..1055573cde 100644
--- a/src/test/recovery/t/050_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/050_standby_failover_slots_sync.pl
@@ -84,6 +84,170 @@ is( $publisher->safe_psql(
 	"t",
 	'logical slot has failover true on the publisher');
 
-$subscriber1->safe_psql('postgres', "DROP SUBSCRIPTION regress_mysub1");
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t",
+	'logical slot has synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+my ($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
 
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index abc944e8b8..b7488d760e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90b37b919c..62554d527e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2584,6 +2585,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



  [application/octet-stream] v71-0006-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../CAJpy0uAv2dS+NTw1X1uZep11WM24Wog2kN1oqJu4Zx4xjpgwig@mail.gmail.com/7-v71-0006-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From 41c98d4f7a3984b34d024e0627be1eb097157522 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v71 6/6] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-29 11:30  Amit Kapila <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-29 11:30 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Peter Smith <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Mon, Jan 29, 2024 at 3:11 PM shveta malik <[email protected]> wrote:
>
> PFA v71 patch set with above changes.
>

Few comments on 0001
===================
1.
parse_subscription_options()
{
...
/*
* We've been explicitly asked to not connect, that requires some
* additional processing.
*/
if (!opts->connect && IsSet(supported_opts, SUBOPT_CONNECT))
{

Here, along with other options, we need an explicit check for
failover, so that if connect=false and failover=true, the statement
should give error. I was expecting the below statement to fail but it
passed with WARNING.
postgres=# create subscription sub2 connection 'dbname=postgres'
publication pub2 with(connect=false, failover=true);
WARNING:  subscription was created, but is not connected
HINT:  To initiate replication, you must manually create the
replication slot, enable the subscription, and refresh the
subscription.
CREATE SUBSCRIPTION

2.
@@ -148,6 +153,10 @@ typedef struct Subscription
  List    *publications; /* List of publication names to subscribe to */
  char    *origin; /* Only publish data originating from the
  * specified origin */
+ bool failover; /* True if the associated replication slots
+ * (i.e. the main slot and the table sync
+ * slots) in the upstream database are enabled
+ * to be synchronized to the standbys. */
 } Subscription;

Let's add this new field immediately after "bool runasowner;" as is
done for other boolean members. This will help avoid increasing the
size of the structure due to alignment when we add any new pointer
field in the future. Also, that would be consistent with what we do
for other new boolean members.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-29 13:17  Zhijie Hou (Fujitsu) <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 3 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-29 13:17 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; shveta malik <[email protected]>; +Cc: Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Monday, January 29, 2024 7:30 PM Amit Kapila <[email protected]> wrote:
> 
> On Mon, Jan 29, 2024 at 3:11 PM shveta malik <[email protected]>
> wrote:
> >
> > PFA v71 patch set with above changes.
> >
> 
> Few comments on 0001

Thanks for the comments.

> ===================
> 1.
> parse_subscription_options()
> {
> ...
> /*
> * We've been explicitly asked to not connect, that requires some
> * additional processing.
> */
> if (!opts->connect && IsSet(supported_opts, SUBOPT_CONNECT)) {
> 
> Here, along with other options, we need an explicit check for failover, so that if
> connect=false and failover=true, the statement should give error. I was
> expecting the below statement to fail but it passed with WARNING.
> postgres=# create subscription sub2 connection 'dbname=postgres'
> publication pub2 with(connect=false, failover=true);
> WARNING:  subscription was created, but is not connected
> HINT:  To initiate replication, you must manually create the replication slot,
> enable the subscription, and refresh the subscription.
> CREATE SUBSCRIPTION

Added.

> 
> 2.
> @@ -148,6 +153,10 @@ typedef struct Subscription
>   List    *publications; /* List of publication names to subscribe to */
>   char    *origin; /* Only publish data originating from the
>   * specified origin */
> + bool failover; /* True if the associated replication slots
> + * (i.e. the main slot and the table sync
> + * slots) in the upstream database are enabled
> + * to be synchronized to the standbys. */
>  } Subscription;
> 
> Let's add this new field immediately after "bool runasowner;" as is done for
> other boolean members. This will help avoid increasing the size of the structure
> due to alignment when we add any new pointer field in the future. Also, that
> would be consistent with what we do for other new boolean members.

Moved this field as suggested.

Attach the V72-0001 which addressed above comments, other patches will be
rebased and posted after pushing first patch. Thanks Shveta for helping address
the comments.

Best Regards,
Hou zj



Attachments:

  [application/octet-stream] v72-0001-Add-a-failover-option-to-subscriptions.patch (65.8K, ../../OS0PR01MB5716303C750A3D2FB71B77CD947E2@OS0PR01MB5716.jpnprd01.prod.outlook.com/2-v72-0001-Add-a-failover-option-to-subscriptions.patch)
  download | inline diff:
From a9a71e1647e08c10d9c6b1a55e3b37797a162942 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:51:39 +0800
Subject: [PATCH v72] Add a failover option to subscriptions.

This commit introduces a new subscription option named 'failover', which
provides users with the ability to set the failover property of the replication
slot on the publisher when creating or altering a subscription.
---
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/ref/alter_subscription.sgml      |  25 ++-
 doc/src/sgml/ref/create_subscription.sgml     |  12 ++
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_views.sql          |   2 +-
 src/backend/commands/subscriptioncmds.c       | 113 ++++++++++++-
 src/backend/replication/logical/tablesync.c   |   2 +-
 src/backend/replication/logical/worker.c      |   7 +
 src/bin/pg_dump/pg_dump.c                     |  18 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_subscription.h         |   9 +
 src/test/recovery/meson.build                 |   1 +
 .../t/040_standby_failover_slots_sync.pl      |  96 +++++++++++
 src/test/regress/expected/subscription.out    | 154 +++++++++---------
 src/test/regress/sql/subscription.sql         |   1 +
 18 files changed, 380 insertions(+), 91 deletions(-)
 create mode 100644 src/test/recovery/t/040_standby_failover_slots_sync.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16b94461b2..880f717b10 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -8000,6 +8000,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..e9e6d9d74a 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,31 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription. Otherwise, the slot on the
+      publisher may behave differently from what subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option says. The slot on the publisher could either be
+      synced to the standbys even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is disabled or could be disabled for sync
+      even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is enabled.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..59a9554bf0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -400,6 +400,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index c62aa0074a..6791bff9dd 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1358,7 +1358,7 @@ REVOKE ALL ON pg_replication_origin_status FROM public;
 REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
-			  subpasswordrequired, subrunasowner,
+			  subpasswordrequired, subrunasowner, subfailover,
               subslotname, subsynccommit, subpublications, suborigin)
     ON pg_subscription TO public;
 
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eaf2ec3b36..7661f9a820 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -71,6 +71,7 @@
 #define SUBOPT_RUN_AS_OWNER			0x00001000
 #define SUBOPT_LSN					0x00002000
 #define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00008000
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -96,6 +97,7 @@ typedef struct SubOpts
 	bool		passwordrequired;
 	bool		runasowner;
 	char	   *origin;
+	bool		failover;
 	XLogRecPtr	lsn;
 } SubOpts;
 
@@ -157,6 +159,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->runasowner = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 
 	/* Parse options */
 	foreach(lc, stmt_options)
@@ -326,6 +330,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						errmsg("unrecognized origin value: \"%s\"", opts->origin));
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_LSN) &&
 				 strcmp(defel->defname, "lsn") == 0)
 		{
@@ -388,6 +401,13 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 					 errmsg("%s and %s are mutually exclusive options",
 							"connect = false", "copy_data = true")));
 
+		if (opts->failover &&
+			IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("%s and %s are mutually exclusive options",
+							"connect = false", "failover = true")));
+
 		/* Change the defaults of other options. */
 		opts->enabled = false;
 		opts->create_slot = false;
@@ -591,7 +611,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+					  SUBOPT_FAILOVER);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -710,6 +731,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 		publicationListToArray(publications);
 	values[Anum_pg_subscription_suborigin - 1] =
 		CStringGetTextDatum(opts.origin);
+	values[Anum_pg_subscription_subfailover - 1] =
+		BoolGetDatum(opts.failover);
 
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -807,7 +830,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +839,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1173,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN |
+								  SUBOPT_FAILOVER);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1218,6 +1260,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_suborigin - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				update_tuple = true;
 				break;
 			}
@@ -1453,6 +1520,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4207b9356c..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,7 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
-					   false,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd..19a3865699 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,17 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 " s.subfailover\n");
+	else
+		appendPQExpBuffer(query,
+						  " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4756,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4801,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5020,6 +5031,9 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	if (strcmp(subinfo->subtwophasestate, two_phase_disabled) != 0)
 		appendPQExpBufferStr(query, ", two_phase = on");
 
+	if (strcmp(subinfo->subfailover, "t") == 0)
+		appendPQExpBufferStr(query, ", failover = true");
+
 	if (strcmp(subinfo->subdisableonerr, "t") == 0)
 		appendPQExpBufferStr(query, ", disable_on_error = true");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 93d97a4090..77db42e354 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -667,6 +667,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9cd8783325..b6a4eb1d56 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6571,7 +6571,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6635,6 +6636,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..0aa14ec4a2 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -142,6 +147,10 @@ typedef struct Subscription
 								 * occurs */
 	bool		passwordrequired;	/* Must connection use a password? */
 	bool		runasowner;		/* Run replication as subscription owner */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 	char	   *conninfo;		/* Connection string to the publisher */
 	char	   *slotname;		/* Name of the replication slot */
 	char	   *synccommit;		/* Synchronous commit setting for worker */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..bf087ac2a9 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/040_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..3b6f7f251b
--- /dev/null
+++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl
@@ -0,0 +1,96 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;");
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+# Enable subscription
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+# Disable failover for enabled subscription
+my ($result, $stdout, $stderr) = $subscriber1->psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+ok( $stderr =~ /ERROR:  cannot set failover for enabled subscription/,
+	"altering failover is not allowed for enabled subscription");
+
+done_testing();
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..1eee6b17b8 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -89,6 +89,8 @@ CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PU
 ERROR:  connect = false and enabled = true are mutually exclusive options
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, create_slot = true);
 ERROR:  connect = false and create_slot = true are mutually exclusive options
+CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+ERROR:  connect = false and failover = true are mutually exclusive options
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = true);
 ERROR:  slot_name = NONE and enabled = true are mutually exclusive options
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = false, create_slot = true);
@@ -116,18 +118,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +147,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +159,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +178,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +190,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +225,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +257,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +281,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +316,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +334,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +373,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +385,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +398,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +414,18 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..1b2a23ba7b 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -54,6 +54,7 @@ SET SESSION AUTHORIZATION 'regress_subscription_user';
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, copy_data = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, enabled = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, create_slot = true);
+CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = false, create_slot = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE);
-- 
2.30.0.windows.2



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-29 13:47  Zhijie Hou (Fujitsu) <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  2 siblings, 0 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-29 13:47 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; shveta malik <[email protected]>; +Cc: Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Monday, January 29, 2024 9:17 PM Zhijie Hou (Fujitsu) <[email protected]> wrote:
> 
> On Monday, January 29, 2024 7:30 PM Amit Kapila <[email protected]>
> wrote:
> >
> > On Mon, Jan 29, 2024 at 3:11 PM shveta malik <[email protected]>
> > wrote:
> > >
> > > PFA v71 patch set with above changes.
> > >
> >
> > Few comments on 0001
> 
> Thanks for the comments.
> 
> > ===================
> > 1.
> > parse_subscription_options()
> > {
> > ...
> > /*
> > * We've been explicitly asked to not connect, that requires some
> > * additional processing.
> > */
> > if (!opts->connect && IsSet(supported_opts, SUBOPT_CONNECT)) {
> >
> > Here, along with other options, we need an explicit check for
> > failover, so that if connect=false and failover=true, the statement
> > should give error. I was expecting the below statement to fail but it passed
> with WARNING.
> > postgres=# create subscription sub2 connection 'dbname=postgres'
> > publication pub2 with(connect=false, failover=true);
> > WARNING:  subscription was created, but is not connected
> > HINT:  To initiate replication, you must manually create the
> > replication slot, enable the subscription, and refresh the subscription.
> > CREATE SUBSCRIPTION
> 
> Added.
> 
> >
> > 2.
> > @@ -148,6 +153,10 @@ typedef struct Subscription
> >   List    *publications; /* List of publication names to subscribe to */
> >   char    *origin; /* Only publish data originating from the
> >   * specified origin */
> > + bool failover; /* True if the associated replication slots
> > + * (i.e. the main slot and the table sync
> > + * slots) in the upstream database are enabled
> > + * to be synchronized to the standbys. */
> >  } Subscription;
> >
> > Let's add this new field immediately after "bool runasowner;" as is
> > done for other boolean members. This will help avoid increasing the
> > size of the structure due to alignment when we add any new pointer
> > field in the future. Also, that would be consistent with what we do for other
> new boolean members.
> 
> Moved this field as suggested.
> 
> Attach the V72-0001 which addressed above comments, other patches will be
> rebased and posted after pushing first patch. Thanks Shveta for helping
> address the comments.

Apart from above comments. The new V72 patch also includes the followings changes.

1. Moved the test 'altering failover for enabled sub' to the tap-test where most
of the alter-sub behaviors are tested.

2. Rename the tap-test from 050_standby_failover_slots_sync.pl to
040_standby_failover_slots_sync.pl (the big number 050 was used to avoid
conflict with other newly committed tests). And add the test into meson.build
which was missed.

Best Regards,
Hou zj


^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-30 01:59  Peter Smith <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  2 siblings, 1 reply; 119+ messages in thread

From: Peter Smith @ 2024-01-30 01:59 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Here are some review comments for v72-0001

======
doc/src/sgml/ref/alter_subscription.sgml

1.
+      parameter value of the subscription. Otherwise, the slot on the
+      publisher may behave differently from what subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option says. The slot on the publisher could either be
+      synced to the standbys even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is disabled or could be disabled for sync
+      even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is enabled.
+     </para>

It is a bit wordy to keep saying "disabled/enabled"

BEFORE
The slot on the publisher could either be synced to the standbys even
when the subscription's failover option is disabled or could be
disabled for sync even when the subscription's failover option is
enabled.

SUGGESTION
The slot on the publisher could be synced to the standbys even when
the subscription's failover = false or may not be syncing even when
the subscription's failover = true.

======
.../t/040_standby_failover_slots_sync.pl

2.
+# Enable subscription
+$subscriber1->safe_psql('postgres',
+ "ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+# Disable failover for enabled subscription
+my ($result, $stdout, $stderr) = $subscriber1->psql('postgres',
+ "ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+ok( $stderr =~ /ERROR:  cannot set failover for enabled subscription/,
+ "altering failover is not allowed for enabled subscription");
+

Currently, those tests are under scope the big comment:

+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################

But that comment is not quite relevant to these tests. So, add another
one just these:

SUGGESTION:
##################################################
# Test that cannot modify the failover option for enabled subscriptions.
##################################################

======
Kind Regards,
Peter Smith.
Fujitsu Australia





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-30 04:06  Amit Kapila <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Amit Kapila @ 2024-01-30 04:06 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; shveta malik <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Tue, Jan 30, 2024 at 7:29 AM Peter Smith <[email protected]> wrote:
>
> Here are some review comments for v72-0001
>
> ======
> doc/src/sgml/ref/alter_subscription.sgml
>
> 1.
> +      parameter value of the subscription. Otherwise, the slot on the
> +      publisher may behave differently from what subscription's
> +      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
> +      option says. The slot on the publisher could either be
> +      synced to the standbys even when the subscription's
> +      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
> +      option is disabled or could be disabled for sync
> +      even when the subscription's
> +      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
> +      option is enabled.
> +     </para>
>
> It is a bit wordy to keep saying "disabled/enabled"
>
> BEFORE
> The slot on the publisher could either be synced to the standbys even
> when the subscription's failover option is disabled or could be
> disabled for sync even when the subscription's failover option is
> enabled.
>
> SUGGESTION
> The slot on the publisher could be synced to the standbys even when
> the subscription's failover = false or may not be syncing even when
> the subscription's failover = true.
>

I think it is a matter of personal preference because I find the
existing wording in the patch easier to follow. So, I would like to
retain that as it is.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-30 06:01  Amit Kapila <[email protected]>
  parent: Zhijie Hou (Fujitsu) <[email protected]>
  2 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-30 06:01 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: shveta malik <[email protected]>; Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Mon, Jan 29, 2024 at 6:47 PM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Monday, January 29, 2024 7:30 PM Amit Kapila <[email protected]> wrote:
> >
>
> > ===================
> > 1.
> > parse_subscription_options()
> > {
> > ...
> > /*
> > * We've been explicitly asked to not connect, that requires some
> > * additional processing.
> > */
> > if (!opts->connect && IsSet(supported_opts, SUBOPT_CONNECT)) {
> >
> > Here, along with other options, we need an explicit check for failover, so that if
> > connect=false and failover=true, the statement should give error. I was
> > expecting the below statement to fail but it passed with WARNING.
> > postgres=# create subscription sub2 connection 'dbname=postgres'
> > publication pub2 with(connect=false, failover=true);
> > WARNING:  subscription was created, but is not connected
> > HINT:  To initiate replication, you must manually create the replication slot,
> > enable the subscription, and refresh the subscription.
> > CREATE SUBSCRIPTION
>
> Added.
>

In this regard, I feel we don't need to dump/restore the 'FAILOVER'
option non-binary upgrade paths similar to the 'ENABLE' option. For
binary upgrade, if the failover option is enabled, then we can enable
it using Alter Subscription SET (failover=true). Let's add one test
corresponding to this behavior in
postgresql\src\bin\pg_upgrade\t\004_subscription.

Additionally, we need to update the pg_dump docs for the 'failover'
option. See "When dumping logical replication subscriptions, .." [1].
I think we also need to update the connect option docs in CREATE
SUBSCRIPTION [2].

[1] - https://www.postgresql.org/docs/devel/app-pgdump.html
[2] - https://www.postgresql.org/docs/devel/sql-createsubscription.html

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-30 10:36  shveta malik <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 2 replies; 119+ messages in thread

From: shveta malik @ 2024-01-30 10:36 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 30, 2024 at 11:31 AM Amit Kapila <[email protected]> wrote:
>
> In this regard, I feel we don't need to dump/restore the 'FAILOVER'
> option non-binary upgrade paths similar to the 'ENABLE' option. For
> binary upgrade, if the failover option is enabled, then we can enable
> it using Alter Subscription SET (failover=true). Let's add one test
> corresponding to this behavior in
> postgresql\src\bin\pg_upgrade\t\004_subscription.

Changed pg_dump behaviour as suggested and added additional test.

> Additionally, we need to update the pg_dump docs for the 'failover'
> option. See "When dumping logical replication subscriptions, .." [1].
> I think we also need to update the connect option docs in CREATE
> SUBSCRIPTION [2].

Updated docs.

> [1] - https://www.postgresql.org/docs/devel/app-pgdump.html
> [2] - https://www.postgresql.org/docs/devel/sql-createsubscription.html

PFA v73-0001 which addresses the above comments. Other patches will be
rebased and posted after pushing this one. Thanks Hou-San for adding
pg_upgrade test for failover.

thanks
Shveta


Attachments:

  [application/octet-stream] v73-0001-Add-a-failover-option-to-subscriptions.patch (71.1K, ../../CAJpy0uBwhbq=c+qYfnOifu0vTo1DvOxDnJCd1ragZxe2K8=nDw@mail.gmail.com/2-v73-0001-Add-a-failover-option-to-subscriptions.patch)
  download | inline diff:
From 3b57e0f76f12cd908a5d224605713eb2e1755d50 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 24 Jan 2024 20:51:39 +0800
Subject: [PATCH v73] Add a failover option to subscriptions.

This commit introduces a new subscription option named 'failover', which
provides users with the ability to set the failover property of the replication
slot on the publisher when creating or altering a subscription.
---
 doc/src/sgml/catalogs.sgml                    |  11 ++
 doc/src/sgml/ref/alter_subscription.sgml      |  25 ++-
 doc/src/sgml/ref/create_subscription.sgml     |  23 ++-
 doc/src/sgml/ref/pg_dump.sgml                 |   8 +-
 src/backend/catalog/pg_subscription.c         |   1 +
 src/backend/catalog/system_views.sql          |   2 +-
 src/backend/commands/subscriptioncmds.c       | 116 ++++++++++++-
 src/backend/replication/logical/tablesync.c   |   2 +-
 src/backend/replication/logical/worker.c      |   7 +
 src/bin/pg_dump/pg_dump.c                     |  21 ++-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_upgrade/t/003_logical_slots.pl     |   6 +-
 src/bin/pg_upgrade/t/004_subscription.pl      |  18 +-
 src/bin/psql/describe.c                       |   8 +-
 src/bin/psql/tab-complete.c                   |   4 +-
 src/include/catalog/pg_subscription.h         |   9 +
 src/test/recovery/meson.build                 |   1 +
 .../t/040_standby_failover_slots_sync.pl      | 100 ++++++++++++
 src/test/regress/expected/subscription.out    | 154 +++++++++---------
 src/test/regress/sql/subscription.sql         |   1 +
 20 files changed, 411 insertions(+), 107 deletions(-)
 create mode 100644 src/test/recovery/t/040_standby_failover_slots_sync.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16b94461b2..880f717b10 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -8000,6 +8000,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subfailover</structfield> <type>bool</type>
+      </para>
+      <para>
+       If true, the associated replication slots (i.e. the main slot and the
+       table sync slots) in the upstream database are enabled to be
+       synchronized to the standbys
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>subconninfo</structfield> <type>text</type>
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 6d36ff0dc9..e9e6d9d74a 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -226,10 +226,31 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
       <link linkend="sql-createsubscription-params-with-streaming"><literal>streaming</literal></link>,
       <link linkend="sql-createsubscription-params-with-disable-on-error"><literal>disable_on_error</literal></link>,
       <link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
-      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>, and
-      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>.
+      <link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
+      <link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>, and
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>.
       Only a superuser can set <literal>password_required = false</literal>.
      </para>
+
+     <para>
+      When altering the
+      <link linkend="sql-createsubscription-params-with-slot-name"><literal>slot_name</literal></link>,
+      the <literal>failover</literal> property value of the named slot may differ from the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter specified in the subscription. When creating the slot,
+      ensure the slot <literal>failover</literal> property matches the
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      parameter value of the subscription. Otherwise, the slot on the
+      publisher may behave differently from what subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option says. The slot on the publisher could either be
+      synced to the standbys even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is disabled or could be disabled for sync
+      even when the subscription's
+      <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>
+      option is enabled.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c7ace922f9..ee89ffb1d1 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -117,19 +117,22 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
           command should connect to the publisher at all.  The default
           is <literal>true</literal>.  Setting this to
           <literal>false</literal> will force the values of
-          <literal>create_slot</literal>, <literal>enabled</literal> and
-          <literal>copy_data</literal> to <literal>false</literal>.
+          <literal>create_slot</literal>, <literal>enabled</literal>,
+          <literal>copy_data</literal>, and <literal>failover</literal>
+          to <literal>false</literal>.
           (You cannot combine setting <literal>connect</literal>
           to <literal>false</literal> with
           setting <literal>create_slot</literal>, <literal>enabled</literal>,
-          or <literal>copy_data</literal> to <literal>true</literal>.)
+          <literal>copy_data</literal>, or <literal>failover</literal> to
+          <literal>true</literal>.)
          </para>
 
          <para>
           Since no connection is made when this option is
           <literal>false</literal>, no tables are subscribed. To initiate
           replication, you must manually create the replication slot, enable
-          the subscription, and refresh the subscription. See
+          the failover if required, enable the subscription, and refresh the
+          subscription. See
           <xref linkend="logical-replication-subscription-examples-deferred-slot"/>
           for examples.
          </para>
@@ -400,6 +403,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry id="sql-createsubscription-params-with-failover">
+        <term><literal>failover</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Specifies whether the replication slots associated with the subscription
+          are enabled to be synced to the standbys so that logical
+          replication can be resumed from the new primary after failover.
+          The default is <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
 
     </listitem>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 0e5ba4f712..d4d65cf059 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1588,7 +1588,13 @@ CREATE DATABASE foo WITH TEMPLATE template0;
    dump can be restored without requiring network access to the remote
    servers.  It is then up to the user to reactivate the subscriptions in a
    suitable way.  If the involved hosts have changed, the connection
-   information might have to be changed.  It might also be appropriate to
+   information might have to be changed.  If the subscription needs to
+   be enabled for
+   <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>,
+   then same needs to be done by executing
+   <link linkend="sql-altersubscription-params-set">
+   <literal>ALTER SUBSCRIPTION ... SET(failover = true)</literal></link>
+   after the slot has been created.  It might also be appropriate to
    truncate the target tables before initiating a new full table copy.  If users
    intend to copy initial data during refresh they must create the slot with
    <literal>two_phase = false</literal>.  After the initial sync, the
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index c516c25ac7..406a3c2dd1 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -73,6 +73,7 @@ GetSubscription(Oid subid, bool missing_ok)
 	sub->disableonerr = subform->subdisableonerr;
 	sub->passwordrequired = subform->subpasswordrequired;
 	sub->runasowner = subform->subrunasowner;
+	sub->failover = subform->subfailover;
 
 	/* Get conninfo */
 	datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index c62aa0074a..6791bff9dd 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1358,7 +1358,7 @@ REVOKE ALL ON pg_replication_origin_status FROM public;
 REVOKE ALL ON pg_subscription FROM public;
 GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
               subbinary, substream, subtwophasestate, subdisableonerr,
-			  subpasswordrequired, subrunasowner,
+			  subpasswordrequired, subrunasowner, subfailover,
               subslotname, subsynccommit, subpublications, suborigin)
     ON pg_subscription TO public;
 
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eaf2ec3b36..b647a81fc8 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,8 +69,10 @@
 #define SUBOPT_DISABLE_ON_ERR		0x00000400
 #define SUBOPT_PASSWORD_REQUIRED	0x00000800
 #define SUBOPT_RUN_AS_OWNER			0x00001000
-#define SUBOPT_LSN					0x00002000
-#define SUBOPT_ORIGIN				0x00004000
+#define SUBOPT_FAILOVER				0x00002000
+#define SUBOPT_LSN					0x00004000
+#define SUBOPT_ORIGIN				0x00008000
+
 
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
@@ -95,6 +97,7 @@ typedef struct SubOpts
 	bool		disableonerr;
 	bool		passwordrequired;
 	bool		runasowner;
+	bool		failover;
 	char	   *origin;
 	XLogRecPtr	lsn;
 } SubOpts;
@@ -155,6 +158,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 		opts->passwordrequired = true;
 	if (IsSet(supported_opts, SUBOPT_RUN_AS_OWNER))
 		opts->runasowner = false;
+	if (IsSet(supported_opts, SUBOPT_FAILOVER))
+		opts->failover = false;
 	if (IsSet(supported_opts, SUBOPT_ORIGIN))
 		opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
 
@@ -303,6 +308,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 			opts->specified_opts |= SUBOPT_RUN_AS_OWNER;
 			opts->runasowner = defGetBoolean(defel);
 		}
+		else if (IsSet(supported_opts, SUBOPT_FAILOVER) &&
+				 strcmp(defel->defname, "failover") == 0)
+		{
+			if (IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+				errorConflictingDefElem(defel, pstate);
+
+			opts->specified_opts |= SUBOPT_FAILOVER;
+			opts->failover = defGetBoolean(defel);
+		}
 		else if (IsSet(supported_opts, SUBOPT_ORIGIN) &&
 				 strcmp(defel->defname, "origin") == 0)
 		{
@@ -388,6 +402,13 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
 					 errmsg("%s and %s are mutually exclusive options",
 							"connect = false", "copy_data = true")));
 
+		if (opts->failover &&
+			IsSet(opts->specified_opts, SUBOPT_FAILOVER))
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("%s and %s are mutually exclusive options",
+							"connect = false", "failover = true")));
+
 		/* Change the defaults of other options. */
 		opts->enabled = false;
 		opts->create_slot = false;
@@ -591,7 +612,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 					  SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
 					  SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
-					  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+					  SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER | SUBOPT_ORIGIN);
 	parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
 
 	/*
@@ -697,6 +718,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 	values[Anum_pg_subscription_subdisableonerr - 1] = BoolGetDatum(opts.disableonerr);
 	values[Anum_pg_subscription_subpasswordrequired - 1] = BoolGetDatum(opts.passwordrequired);
 	values[Anum_pg_subscription_subrunasowner - 1] = BoolGetDatum(opts.runasowner);
+	values[Anum_pg_subscription_subfailover - 1] = BoolGetDatum(opts.failover);
 	values[Anum_pg_subscription_subconninfo - 1] =
 		CStringGetTextDatum(conninfo);
 	if (opts.slot_name)
@@ -807,7 +829,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 					twophase_enabled = true;
 
 				walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled,
-								   false, CRS_NOEXPORT_SNAPSHOT, NULL);
+								   opts.failover, CRS_NOEXPORT_SNAPSHOT, NULL);
 
 				if (twophase_enabled)
 					UpdateTwoPhaseState(subid, LOGICALREP_TWOPHASE_STATE_ENABLED);
@@ -816,6 +838,24 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 						(errmsg("created replication slot \"%s\" on publisher",
 								opts.slot_name)));
 			}
+
+			/*
+			 * If the slot_name is specified without the create_slot option,
+			 * it is possible that the user intends to use an existing slot on
+			 * the publisher, so here we alter the failover property of the
+			 * slot to match the failover value in subscription.
+			 *
+			 * We do not need to change the failover to false if the server
+			 * does not support failover (e.g. pre-PG17).
+			 */
+			else if (opts.slot_name &&
+					 (opts.failover || walrcv_server_version(wrconn) >= 170000))
+			{
+				walrcv_alter_slot(wrconn, opts.slot_name, opts.failover);
+				ereport(NOTICE,
+						(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+								opts.slot_name, opts.failover ? "true" : "false")));
+			}
 		}
 		PG_FINALLY();
 		{
@@ -1132,7 +1172,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 								  SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
 								  SUBOPT_STREAMING | SUBOPT_DISABLE_ON_ERR |
 								  SUBOPT_PASSWORD_REQUIRED |
-								  SUBOPT_RUN_AS_OWNER | SUBOPT_ORIGIN);
+								  SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER |
+								  SUBOPT_ORIGIN);
 
 				parse_subscription_options(pstate, stmt->options,
 										   supported_opts, &opts);
@@ -1211,6 +1252,31 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 					replaces[Anum_pg_subscription_subrunasowner - 1] = true;
 				}
 
+				if (IsSet(opts.specified_opts, SUBOPT_FAILOVER))
+				{
+					if (!sub->slotname)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for a subscription that does not have a slot name",
+										"failover")));
+
+					/*
+					 * Do not allow changing the failover state if the
+					 * subscription is enabled. This is because the failover
+					 * state of the slot on the publisher cannot be modified
+					 * if the slot is currently acquired by the apply worker.
+					 */
+					if (sub->enabled)
+						ereport(ERROR,
+								(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								 errmsg("cannot set %s for enabled subscription",
+										"failover")));
+
+					values[Anum_pg_subscription_subfailover - 1] =
+						BoolGetDatum(opts.failover);
+					replaces[Anum_pg_subscription_subfailover - 1] = true;
+				}
+
 				if (IsSet(opts.specified_opts, SUBOPT_ORIGIN))
 				{
 					values[Anum_pg_subscription_suborigin - 1] =
@@ -1453,6 +1519,46 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 		heap_freetuple(tup);
 	}
 
+	/*
+	 * Try to acquire the connection necessary for altering slot.
+	 *
+	 * This has to be at the end because otherwise if there is an error while
+	 * doing the database operations we won't be able to rollback altered
+	 * slot.
+	 */
+	if (replaces[Anum_pg_subscription_subfailover - 1])
+	{
+		bool		must_use_password;
+		char	   *err;
+		WalReceiverConn *wrconn;
+
+		/* Load the library providing us libpq calls. */
+		load_file("libpqwalreceiver", false);
+
+		/* Try to connect to the publisher. */
+		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
+		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+								sub->name, &err);
+		if (!wrconn)
+			ereport(ERROR,
+					(errcode(ERRCODE_CONNECTION_FAILURE),
+					 errmsg("could not connect to the publisher: %s", err)));
+
+		PG_TRY();
+		{
+			walrcv_alter_slot(wrconn, sub->slotname, opts.failover);
+
+			ereport(NOTICE,
+					(errmsg("changed the failover state of replication slot \"%s\" on publisher to %s",
+							sub->slotname, opts.failover ? "true" : "false")));
+		}
+		PG_FINALLY();
+		{
+			walrcv_disconnect(wrconn);
+		}
+		PG_END_TRY();
+	}
+
 	table_close(rel, RowExclusiveLock);
 
 	ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4207b9356c..5acab3f3e2 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1430,7 +1430,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 */
 	walrcv_create_slot(LogRepWorkerWalRcvConn,
 					   slotname, false /* permanent */ , false /* two_phase */ ,
-					   false,
+					   MySubscription->failover,
 					   CRS_USE_SNAPSHOT, origin_startpos);
 
 	/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 9b598caf3c..32ff4c0336 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -132,6 +132,13 @@
  * avoid such deadlocks, we generate a unique GID (consisting of the
  * subscription oid and the xid of the prepared transaction) for each prepare
  * transaction on the subscriber.
+ *
+ * FAILOVER
+ * ----------------------
+ * The logical slot on the primary can be synced to the standby by specifying
+ * failover = true when creating the subscription. Enabling failover allows us
+ * to smoothly transition to the promoted standby, ensuring that we can
+ * subscribe to the new primary without losing any data.
  *-------------------------------------------------------------------------
  */
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd..348748bae5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4641,6 +4641,7 @@ getSubscriptions(Archive *fout)
 	int			i_suborigin;
 	int			i_suboriginremotelsn;
 	int			i_subenabled;
+	int			i_subfailover;
 	int			i,
 				ntups;
 
@@ -4706,10 +4707,12 @@ getSubscriptions(Archive *fout)
 
 	if (dopt->binary_upgrade && fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query, " o.remote_lsn AS suboriginremotelsn,\n"
-							 " s.subenabled\n");
+							 " s.subenabled,\n"
+							 " s.subfailover\n");
 	else
 		appendPQExpBufferStr(query, " NULL AS suboriginremotelsn,\n"
-							 " false AS subenabled\n");
+							 " false AS subenabled,\n"
+							 " false AS subfailover\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -4748,6 +4751,7 @@ getSubscriptions(Archive *fout)
 	i_suborigin = PQfnumber(res, "suborigin");
 	i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn");
 	i_subenabled = PQfnumber(res, "subenabled");
+	i_subfailover = PQfnumber(res, "subfailover");
 
 	subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo));
 
@@ -4792,6 +4796,8 @@ getSubscriptions(Archive *fout)
 				pg_strdup(PQgetvalue(res, i, i_suboriginremotelsn));
 		subinfo[i].subenabled =
 			pg_strdup(PQgetvalue(res, i, i_subenabled));
+		subinfo[i].subfailover =
+			pg_strdup(PQgetvalue(res, i, i_subfailover));
 
 		/* Decide whether we want to dump it */
 		selectDumpableObject(&(subinfo[i].dobj), fout);
@@ -5062,6 +5068,17 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 			appendPQExpBuffer(query, ", '%s');\n", subinfo->suboriginremotelsn);
 		}
 
+		if (strcmp(subinfo->subfailover, "t") == 0)
+		{
+			/*
+			 * Enable the failover to allow the subscription's slot to be
+			 * synced to the standbys after the upgrade.
+			 */
+			appendPQExpBufferStr(query,
+								 "\n-- For binary upgrade, must preserve the subscriber's failover option.\n");
+			appendPQExpBuffer(query, "ALTER SUBSCRIPTION %s SET(failover = true);\n", qsubname);
+		}
+
 		if (strcmp(subinfo->subenabled, "t") == 0)
 		{
 			/*
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 93d97a4090..77db42e354 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -667,6 +667,7 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 	char	   *suborigin;
 	char	   *suboriginremotelsn;
+	char	   *subfailover;
 } SubscriptionInfo;
 
 /*
diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl
index 0ab368247b..83d71c3084 100644
--- a/src/bin/pg_upgrade/t/003_logical_slots.pl
+++ b/src/bin/pg_upgrade/t/003_logical_slots.pl
@@ -172,7 +172,7 @@ $sub->start;
 $sub->safe_psql(
 	'postgres', qq[
 	CREATE TABLE tbl (a int);
-	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true')
+	CREATE SUBSCRIPTION regress_sub CONNECTION '$old_connstr' PUBLICATION regress_pub WITH (two_phase = 'true', failover = 'true')
 ]);
 $sub->wait_for_subscription_sync($oldpub, 'regress_sub');
 
@@ -192,8 +192,8 @@ command_ok([@pg_upgrade_cmd], 'run of pg_upgrade of old cluster');
 # Check that the slot 'regress_sub' has migrated to the new cluster
 $newpub->start;
 my $result = $newpub->safe_psql('postgres',
-	"SELECT slot_name, two_phase FROM pg_replication_slots");
-is($result, qq(regress_sub|t), 'check the slot exists on new cluster');
+	"SELECT slot_name, two_phase, failover FROM pg_replication_slots");
+is($result, qq(regress_sub|t|t), 'check the slot exists on new cluster');
 
 # Update the connection
 my $new_connstr = $newpub->connstr . ' dbname=postgres';
diff --git a/src/bin/pg_upgrade/t/004_subscription.pl b/src/bin/pg_upgrade/t/004_subscription.pl
index 9e8f9b7970..bd26f47165 100644
--- a/src/bin/pg_upgrade/t/004_subscription.pl
+++ b/src/bin/pg_upgrade/t/004_subscription.pl
@@ -49,11 +49,11 @@ $old_sub->safe_psql(
 # Setup logical replication
 my $connstr = $publisher->connstr . ' dbname=postgres';
 
-# Setup an enabled subscription to verify that the running status is retained
-# after upgrade.
+# Setup an enabled subscription to verify that the running status and failover
+# option is retained after upgrade.
 $publisher->safe_psql('postgres', "CREATE PUBLICATION regress_pub1");
 $old_sub->safe_psql('postgres',
-	"CREATE SUBSCRIPTION regress_sub1 CONNECTION '$connstr' PUBLICATION regress_pub1"
+	"CREATE SUBSCRIPTION regress_sub1 CONNECTION '$connstr' PUBLICATION regress_pub1 WITH (failover = true)"
 );
 $old_sub->wait_for_subscription_sync($publisher, 'regress_sub1');
 
@@ -137,14 +137,14 @@ $publisher->safe_psql(
 
 $new_sub->start;
 
-# The subscription's running status should be preserved. Old subscription
-# regress_sub1 should be enabled and old subscription regress_sub2 should be
-# disabled.
+# The subscription's running status and failover option should be preserved.
+# Old subscription regress_sub1 should have enabled and failover as true while
+# old subscription regress_sub2 should have enabled and failover as false.
 $result =
   $new_sub->safe_psql('postgres',
-	"SELECT subname, subenabled FROM pg_subscription ORDER BY subname");
-is( $result, qq(regress_sub1|t
-regress_sub2|f),
+	"SELECT subname, subenabled, subfailover FROM pg_subscription ORDER BY subname");
+is( $result, qq(regress_sub1|t|t
+regress_sub2|f|f),
 	"check that the subscription's running status are preserved");
 
 my $sub_oid = $new_sub->safe_psql('postgres',
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9cd8783325..b6a4eb1d56 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -6571,7 +6571,8 @@ describeSubscriptions(const char *pattern, bool verbose)
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
 	static const bool translate_columns[] = {false, false, false, false,
-	false, false, false, false, false, false, false, false, false, false};
+		false, false, false, false, false, false, false, false, false, false,
+	false};
 
 	if (pset.sversion < 100000)
 	{
@@ -6635,6 +6636,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 							  gettext_noop("Password required"),
 							  gettext_noop("Run as owner?"));
 
+		if (pset.sversion >= 170000)
+			appendPQExpBuffer(&buf,
+							  ", subfailover AS \"%s\"\n",
+							  gettext_noop("Failover"));
+
 		appendPQExpBuffer(&buf,
 						  ",  subsynccommit AS \"%s\"\n"
 						  ",  subconninfo AS \"%s\"\n",
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index ada711d02f..151a5211ee 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1943,7 +1943,7 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
 	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) && TailMatches("SET", "("))
-		COMPLETE_WITH("binary", "disable_on_error", "origin",
+		COMPLETE_WITH("binary", "disable_on_error", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit");
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
@@ -3340,7 +3340,7 @@ psql_completion(const char *text, int start, int end)
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
 	else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
 		COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
-					  "disable_on_error", "enabled", "origin",
+					  "disable_on_error", "enabled", "failover", "origin",
 					  "password_required", "run_as_owner", "slot_name",
 					  "streaming", "synchronous_commit", "two_phase");
 
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index ab206bad7d..0aa14ec4a2 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -93,6 +93,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW
 	bool		subrunasowner;	/* True if replication should execute as the
 								 * subscription owner */
 
+	bool		subfailover;	/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* Connection string to the publisher */
 	text		subconninfo BKI_FORCE_NOT_NULL;
@@ -142,6 +147,10 @@ typedef struct Subscription
 								 * occurs */
 	bool		passwordrequired;	/* Must connection use a password? */
 	bool		runasowner;		/* Run replication as subscription owner */
+	bool		failover;		/* True if the associated replication slots
+								 * (i.e. the main slot and the table sync
+								 * slots) in the upstream database are enabled
+								 * to be synchronized to the standbys. */
 	char	   *conninfo;		/* Connection string to the publisher */
 	char	   *slotname;		/* Name of the replication slot */
 	char	   *synccommit;		/* Synchronous commit setting for worker */
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 88fb0306f5..bf087ac2a9 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/037_invalid_database.pl',
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
+      't/040_standby_failover_slots_sync.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl
new file mode 100644
index 0000000000..bc58ff4cab
--- /dev/null
+++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl
@@ -0,0 +1,100 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+##################################################
+# Test that when a subscription with failover enabled is created, it will alter
+# the failover property of the corresponding slot on the publisher.
+##################################################
+
+# Create publisher
+my $publisher = PostgreSQL::Test::Cluster->new('publisher');
+$publisher->init(allows_streaming => 'logical');
+$publisher->start;
+
+$publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;");
+
+my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
+
+# Create a subscriber node, wait for sync to complete
+my $subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
+$subscriber1->init;
+$subscriber1->start;
+
+# Create a slot on the publisher with failover disabled
+$publisher->safe_psql('postgres',
+	"SELECT 'init' FROM pg_create_logical_replication_slot('lsub1_slot', 'pgoutput', false, false, false);"
+);
+
+# Confirm that the failover flag on the slot is turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Create a subscription (using the same slot created above) that enables
+# failover.
+$subscriber1->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, copy_data=false, failover = true, create_slot = false, enabled = false);"
+);
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that changing the failover property of a subscription updates the
+# corresponding failover property of the slot.
+##################################################
+
+# Disable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+
+# Confirm that the failover flag on the slot has now been turned off
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"f",
+	'logical slot has failover false on the publisher');
+
+# Enable failover
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = true)");
+
+# Confirm that the failover flag on the slot has now been turned on
+is( $publisher->safe_psql(
+		'postgres',
+		q{SELECT failover from pg_replication_slots WHERE slot_name = 'lsub1_slot';}
+	),
+	"t",
+	'logical slot has failover true on the publisher');
+
+##################################################
+# Test that the failover option cannot be changed for enabled subscriptions.
+##################################################
+
+# Enable subscription
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 ENABLE");
+
+# Disable failover for enabled subscription
+my ($result, $stdout, $stderr) = $subscriber1->psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)");
+ok( $stderr =~ /ERROR:  cannot set failover for enabled subscription/,
+	"altering failover is not allowed for enabled subscription");
+
+done_testing();
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index b15eddbff3..1eee6b17b8 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -89,6 +89,8 @@ CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PU
 ERROR:  connect = false and enabled = true are mutually exclusive options
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, create_slot = true);
 ERROR:  connect = false and create_slot = true are mutually exclusive options
+CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
+ERROR:  connect = false and failover = true are mutually exclusive options
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = true);
 ERROR:  slot_name = NONE and enabled = true are mutually exclusive options
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = false, create_slot = true);
@@ -116,18 +118,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | none   | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
 \dRs+ regress_testsub4
-                                                                                                           List of subscriptions
-       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
-------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                 List of subscriptions
+       Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub4 | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub3;
@@ -145,10 +147,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
 ERROR:  invalid connection string syntax: missing "=" after "foobar" in connection info string
 
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
@@ -157,10 +159,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
 ALTER SUBSCRIPTION regress_testsub SET (password_required = false);
 ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true);
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | f                 | t             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (password_required = true);
@@ -176,10 +178,10 @@ ERROR:  unrecognized subscription parameter: "create_slot"
 -- ok
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345');
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/12345
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/12345
 (1 row)
 
 -- ok - with lsn = NONE
@@ -188,10 +190,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE);
 ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0');
 ERROR:  invalid WAL location (LSN): 0/0
 \dRs+
-                                                                                                               List of subscriptions
-      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                     List of subscriptions
+      Name       |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 BEGIN;
@@ -223,10 +225,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
 ERROR:  invalid value for parameter "synchronous_commit": "foobar"
 HINT:  Available values: local, remote_write, remote_apply, on, off.
 \dRs+
-                                                                                                                 List of subscriptions
-        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |           Conninfo           | Skip LSN 
----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+------------------------------+----------
- regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | local              | dbname=regress_doesnotexist2 | 0/0
+                                                                                                                       List of subscriptions
+        Name         |           Owner           | Enabled |     Publication     | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |           Conninfo           | Skip LSN 
+---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+----------
+ regress_testsub_foo | regress_subscription_user | f       | {testpub2,testpub3} | f      | off       | d                | f                | any    | t                 | f             | f        | local              | dbname=regress_doesnotexist2 | 0/0
 (1 row)
 
 -- rename back to keep the rest simple
@@ -255,19 +257,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | t      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (binary = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -279,27 +281,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | parallel  | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (streaming = false);
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication already exists
@@ -314,10 +316,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr
 ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false);
 ERROR:  publication "testpub1" is already in subscription "regress_testsub"
 \dRs+
-                                                                                                                   List of subscriptions
-      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                        List of subscriptions
+      Name       |           Owner           | Enabled |         Publication         | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub,testpub1,testpub2} | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 -- fail - publication used more than once
@@ -332,10 +334,10 @@ ERROR:  publication "testpub3" is not in subscription "regress_testsub"
 -- ok - delete publications
 ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 DROP SUBSCRIPTION regress_testsub;
@@ -371,10 +373,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 --fail - alter of two_phase option not supported.
@@ -383,10 +385,10 @@ ERROR:  unrecognized subscription parameter: "two_phase"
 -- but can alter streaming when two_phase enabled
 ALTER SUBSCRIPTION regress_testsub SET (streaming = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -396,10 +398,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | on        | p                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
@@ -412,18 +414,18 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB
 WARNING:  subscription was created, but is not connected
 HINT:  To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | f                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true);
 \dRs+
-                                                                                                           List of subscriptions
-      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Synchronous commit |          Conninfo           | Skip LSN 
------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+--------------------+-----------------------------+----------
- regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | off                | dbname=regress_doesnotexist | 0/0
+                                                                                                                List of subscriptions
+      Name       |           Owner           | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit |          Conninfo           | Skip LSN 
+-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+----------
+ regress_testsub | regress_subscription_user | f       | {testpub}   | f      | off       | d                | t                | any    | t                 | f             | f        | off                | dbname=regress_doesnotexist | 0/0
 (1 row)
 
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 444e563ff3..1b2a23ba7b 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -54,6 +54,7 @@ SET SESSION AUTHORIZATION 'regress_subscription_user';
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, copy_data = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, enabled = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, create_slot = true);
+CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, failover = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = false, create_slot = true);
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE);
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-30 12:52  shveta malik <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: shveta malik @ 2024-01-30 12:52 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>; shveta malik <[email protected]>

On Tue, Jan 30, 2024 at 4:06 PM shveta malik <[email protected]> wrote:
>
> PFA v73-0001 which addresses the above comments. Other patches will be
> rebased and posted after pushing this one.

Since v73-0001 is pushed, PFA  rest of the patches. Changes are:

1) Rebased the patches.
2) Ran pg_indent on all.
3) patch001: Updated logicaldecoding.sgml for dbname requirement in
primary_conninfo for slot-synchronization.

thanks
Shveta


Attachments:

  [application/octet-stream] v74-0002-Slot-sync-worker-as-a-special-process.patch (38.4K, ../../CAJpy0uBG6JgFSM7Bt0-1D+6u1H9K2+C6wvBanfiWcK9TupHnFw@mail.gmail.com/2-v74-0002-Slot-sync-worker-as-a-special-process.patch)
  download | inline diff:
From 3b506f1087963611be8bf73392a3c2927cacb0e4 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 30 Jan 2024 17:08:04 +0530
Subject: [PATCH v74 2/5] Slot-sync worker as a special process

This patch attempts to start slot-sync worker as a special process
which is neither a bgworker nor an Auxiliary process. The benefit
we get here is we can control the start-conditions of the worker which
further allows us to define 'enable_syncslot' as PGC_SIGHUP which was
otherwise a PGC_POSTMASTER GUC when slotsync worker was registered as
bgworker.
---
 doc/src/sgml/bgworker.sgml                    |  65 +---
 src/backend/postmaster/bgworker.c             |   3 -
 src/backend/postmaster/postmaster.c           |  86 +++--
 src/backend/replication/logical/slotsync.c    | 359 ++++++++++++++----
 src/backend/storage/lmgr/proc.c               |  13 +-
 src/backend/tcop/postgres.c                   |  11 -
 src/backend/utils/activity/pgstat_io.c        |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/miscinit.c             |   9 +-
 src/backend/utils/init/postinit.c             |   8 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/miscadmin.h                       |   1 +
 src/include/postmaster/bgworker.h             |   1 -
 src/include/replication/worker_internal.h     |   8 +-
 14 files changed, 387 insertions(+), 181 deletions(-)

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index a7cfe6c58c..2c393385a9 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,59 +114,18 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process. Note that this setting
-   only indicates when the processes are to be started; they do not stop when
-   a different state is reached. Possible values are:
-
-   <variablelist>
-    <varlistentry>
-     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
-       Start as soon as postgres itself has finished its own initialization;
-       processes requesting this are not eligible for database connections.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
-       Start as soon as a consistent state has been reached in a hot-standby,
-       allowing processes to connect to databases and run read-only queries.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
-       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
-       it is more strict in terms of the server i.e. start the worker only
-       if it is hot-standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
-     <listitem>
-      <para>
-       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
-       Start as soon as the system has entered normal read-write state. Note
-       that the <literal>BgWorkerStart_ConsistentState</literal> and
-      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
-       in a server that's not a hot standby.
-      </para>
-     </listitem>
-    </varlistentry>
-
-   </variablelist>
+   <command>postgres</command> should start the process; it can be one of
+   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
+   <command>postgres</command> itself has finished its own initialization; processes
+   requesting this are not eligible for database connections),
+   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
+   has been reached in a hot standby, allowing processes to connect to
+   databases and run read-only queries), and
+   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
+   entered normal read-write state).  Note the last two values are equivalent
+   in a server that's not a hot standby.  Note that this setting only indicates
+   when the processes are to be started; they do not stop when a different state
+   is reached.
   </para>
 
   <para>
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 46828b8a89..1add449f7c 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -130,9 +130,6 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
-	{
-		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
-	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 36795f91f0..adfaf75cf9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -168,11 +168,11 @@
  * they will never become live backends.  dead_end children are not assigned a
  * PMChildSlot.  dead_end children have bkend_type NORMAL.
  *
- * "Special" children such as the startup, bgwriter and autovacuum launcher
- * tasks are not in this list.  They are tracked via StartupPID and other
- * pid_t variables below.  (Thus, there can't be more than one of any given
- * "special" child process type.  We use BackendList entries for any child
- * process there can be more than one of.)
+ * "Special" children such as the startup, bgwriter, autovacuum launcher and
+ * slot sync worker tasks are not in this list.  They are tracked via StartupPID
+ * and other pid_t variables below.  (Thus, there can't be more than one of any
+ * given "special" child process type.  We use BackendList entries for any
+ * child process there can be more than one of.)
  */
 typedef struct bkend
 {
@@ -255,7 +255,8 @@ static pid_t StartupPID = 0,
 			WalSummarizerPID = 0,
 			AutoVacPID = 0,
 			PgArchPID = 0,
-			SysLoggerPID = 0;
+			SysLoggerPID = 0,
+			SlotSyncWorkerPID = 0;
 
 /* Startup process's status */
 typedef enum
@@ -459,6 +460,10 @@ static void InitPostmasterDeathWatchHandle(void);
 	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
 	 PgArchCanRestart())
 
+#define SlotSyncWorkerAllowed()	\
+	(enable_syncslot && pmState == PM_HOT_STANDBY && \
+	 SlotSyncWorkerCanRestart())
+
 #ifdef EXEC_BACKEND
 
 #ifdef WIN32
@@ -1011,12 +1016,6 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
-	/*
-	 * Register the slot sync worker here to kick start slot-sync operation
-	 * sooner on the physical standby.
-	 */
-	SlotSyncWorkerRegister();
-
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -1837,6 +1836,10 @@ ServerLoop(void)
 		if (PgArchPID == 0 && PgArchStartupAllowed())
 			PgArchPID = StartArchiver();
 
+		/* If we need to start a slot sync worker, try to do that now */
+		if (SlotSyncWorkerPID == 0 && SlotSyncWorkerAllowed())
+			SlotSyncWorkerPID = StartSlotSyncWorker();
+
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
 		{
@@ -2684,6 +2687,8 @@ process_pm_reload_request(void)
 			signal_child(PgArchPID, SIGHUP);
 		if (SysLoggerPID != 0)
 			signal_child(SysLoggerPID, SIGHUP);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -3041,6 +3046,8 @@ process_pm_child_exit(void)
 				AutoVacPID = StartAutoVacLauncher();
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartArchiver();
+			if (SlotSyncWorkerAllowed() && SlotSyncWorkerPID == 0)
+				SlotSyncWorkerPID = StartSlotSyncWorker();
 
 			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
@@ -3211,6 +3218,22 @@ process_pm_child_exit(void)
 			continue;
 		}
 
+		/*
+		 * Was it the slot sync worker? Normal exit or FATAL exit can be
+		 * ignored (FATAL can be caused by libpqwalreceiver on receiving
+		 * shutdown request by the startup process during promotion); we'll
+		 * start a new one at the next iteration of the postmaster's main
+		 * loop, if necessary. Any other exit condition is treated as a crash.
+		 */
+		if (pid == SlotSyncWorkerPID)
+		{
+			SlotSyncWorkerPID = 0;
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus,
+								 _("slot sync worker process"));
+			continue;
+		}
+
 		/* Was it one of our background workers? */
 		if (CleanupBackgroundWorker(pid, exitstatus))
 		{
@@ -3577,6 +3600,12 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	else if (PgArchPID != 0 && take_action)
 		sigquit_child(PgArchPID);
 
+	/* Take care of the slot sync worker too */
+	if (pid == SlotSyncWorkerPID)
+		SlotSyncWorkerPID = 0;
+	else if (SlotSyncWorkerPID != 0 && take_action)
+		sigquit_child(SlotSyncWorkerPID);
+
 	/* We do NOT restart the syslogger */
 
 	if (Shutdown != ImmediateShutdown)
@@ -3717,6 +3746,8 @@ PostmasterStateMachine(void)
 			signal_child(WalReceiverPID, SIGTERM);
 		if (WalSummarizerPID != 0)
 			signal_child(WalSummarizerPID, SIGTERM);
+		if (SlotSyncWorkerPID != 0)
+			signal_child(SlotSyncWorkerPID, SIGTERM);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
@@ -3732,13 +3763,13 @@ PostmasterStateMachine(void)
 		/*
 		 * PM_WAIT_BACKENDS state ends when we have no regular backends
 		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher or bgwriter.  If we are
-		 * doing crash recovery or an immediate shutdown then we expect the
-		 * checkpointer to exit as well, otherwise not. The stats and
-		 * syslogger processes are disregarded since they are not connected to
-		 * shared memory; we also disregard dead_end children here. Walsenders
-		 * and archiver are also disregarded, they will be terminated later
-		 * after writing the checkpoint record.
+		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
+		 * worker.  If we are doing crash recovery or an immediate shutdown
+		 * then we expect the checkpointer to exit as well, otherwise not. The
+		 * stats and syslogger processes are disregarded since they are not
+		 * connected to shared memory; we also disregard dead_end children
+		 * here. Walsenders and archiver are also disregarded, they will be
+		 * terminated later after writing the checkpoint record.
 		 */
 		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
 			StartupPID == 0 &&
@@ -3748,7 +3779,8 @@ PostmasterStateMachine(void)
 			(CheckpointerPID == 0 ||
 			 (!FatalError && Shutdown < ImmediateShutdown)) &&
 			WalWriterPID == 0 &&
-			AutoVacPID == 0)
+			AutoVacPID == 0 &&
+			SlotSyncWorkerPID == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -3846,6 +3878,7 @@ PostmasterStateMachine(void)
 			Assert(CheckpointerPID == 0);
 			Assert(WalWriterPID == 0);
 			Assert(AutoVacPID == 0);
+			Assert(SlotSyncWorkerPID == 0);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -4069,6 +4102,8 @@ TerminateChildren(int signal)
 		signal_child(AutoVacPID, signal);
 	if (PgArchPID != 0)
 		signal_child(PgArchPID, signal);
+	if (SlotSyncWorkerPID != 0)
+		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -4881,6 +4916,7 @@ SubPostmasterMain(int argc, char *argv[])
 	 */
 	if (strcmp(argv[1], "--forkbackend") == 0 ||
 		strcmp(argv[1], "--forkavlauncher") == 0 ||
+		strcmp(argv[1], "--forkssworker") == 0 ||
 		strcmp(argv[1], "--forkavworker") == 0 ||
 		strcmp(argv[1], "--forkaux") == 0 ||
 		strcmp(argv[1], "--forkbgworker") == 0)
@@ -4984,6 +5020,13 @@ SubPostmasterMain(int argc, char *argv[])
 
 		AutoVacWorkerMain(argc - 2, argv + 2);	/* does not return */
 	}
+	if (strcmp(argv[1], "--forkssworker") == 0)
+	{
+		/* Restore basic shared memory pointers */
+		InitShmemAccess(UsedShmemSegAddr);
+
+		ReplSlotSyncWorkerMain(argc - 2, argv + 2); /* does not return */
+	}
 	if (strcmp(argv[1], "--forkbgworker") == 0)
 	{
 		/* do this as early as possible; in particular, before InitProcess() */
@@ -5806,9 +5849,6 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
-			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
-				pmState != PM_RUN)
-				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index e44474dd58..76a73aa680 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -34,15 +34,20 @@
 
 #include "postgres.h"
 
+#include <time.h>
+
 #include "access/genam.h"
 #include "access/table.h"
 #include "access/xlog_internal.h"
 #include "access/xlogrecovery.h"
 #include "catalog/pg_database.h"
 #include "commands/dbcommands.h"
+#include "libpq/pqsignal.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
+#include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
+#include "postmaster/postmaster.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -56,6 +61,8 @@
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/pg_lsn.h"
+#include "utils/ps_status.h"
+#include "utils/timeout.h"
 #include "utils/varlena.h"
 
 /*
@@ -109,8 +116,16 @@ bool		enable_syncslot = false;
 #define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
 static long sleep_ms = MIN_WORKER_NAPTIME_MS;
 
+/* Flag to tell if we are in a slot sync worker process */
+static bool am_slotsync_worker = false;
+
 static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
 
+#ifdef EXEC_BACKEND
+static pid_t slotsyncworker_forkexec(void);
+#endif
+NON_EXEC_STATIC void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+
 /*
  * If necessary, update local slot metadata based on the data from the remote
  * slot.
@@ -734,7 +749,8 @@ synchronize_slots(WalReceiverConn *wrconn)
  * standbys.
  */
 static void
-check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby,
+				   bool *primary_slot_invalid)
 {
 #define PRIMARY_INFO_OUTPUT_COL_COUNT 2
 	WalRcvExecResult *res;
@@ -749,8 +765,10 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 	StartTransactionCommand();
 
 	Assert(am_cascading_standby != NULL);
+	Assert(primary_slot_invalid != NULL);
 
 	*am_cascading_standby = false;	/* overwritten later if cascading */
+	*primary_slot_invalid = false;	/* overwritten later if invalid */
 
 	initStringInfo(&cmd);
 	appendStringInfo(&cmd,
@@ -788,13 +806,16 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 		Assert(!isnull);
 
 		if (!valid)
-			ereport(ERROR,
+		{
+			*primary_slot_invalid = true;
+			ereport(LOG,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("exiting from slot synchronization due to bad configuration"),
+					errmsg("bad configuration for slot synchronization"),
 			/* translator: second %s is a GUC variable name */
 					errdetail("The primary server slot \"%s\" specified by"
 							  " \"%s\" is not valid.",
 							  PrimarySlotName, "primary_slot_name"));
+		}
 	}
 
 	ExecClearTuple(tupslot);
@@ -803,20 +824,15 @@ check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
 }
 
 /*
- * Check that all necessary GUCs for slot synchronization are set
- * appropriately. If not, raise an ERROR.
+ * Returns true if all necessary GUCs for slot synchronization are set
+ * appropriately, otherwise returns false.
  *
  * If all checks pass, extracts the dbname from the primary_conninfo GUC and
- * returns it.
+ * and return it in output dbname arg.
  */
-static char *
-validate_parameters_and_get_dbname(void)
+static bool
+validate_parameters_and_get_dbname(char **dbname)
 {
-	char	   *dbname;
-
-	/* Sanity check. */
-	Assert(enable_syncslot);
-
 	/*
 	 * A physical replication slot(primary_slot_name) is required on the
 	 * primary to ensure that the rows needed by the standby are not removed
@@ -824,11 +840,14 @@ validate_parameters_and_get_dbname(void)
 	 * be invalidated.
 	 */
 	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_slot_name"));
+		return false;
+	}
 
 	/*
 	 * hot_standby_feedback must be enabled to cooperate with the physical
@@ -836,49 +855,98 @@ validate_parameters_and_get_dbname(void)
 	 * catalog_xmin values on the standby.
 	 */
 	if (!hot_standby_feedback)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+		return false;
+	}
 
 	/*
 	 * Logical decoding requires wal_level >= logical and we currently only
 	 * synchronize logical slots.
 	 */
 	if (wal_level < WAL_LEVEL_LOGICAL)
-		ereport(ERROR,
-		/* translator: %s is a GUC variable name */
+	{
+		ereport(LOG,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"wal_level\" must be >= logical."));
+		return false;
+	}
 
 	/*
 	 * The primary_conninfo is required to make connection to primary for
 	 * getting slots information.
 	 */
 	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
-		ereport(ERROR,
+	{
+		ereport(LOG,
 		/* translator: %s is a GUC variable name */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("\"%s\" must be defined.", "primary_conninfo"));
+		return false;
+	}
 
 	/*
 	 * The slot sync worker needs a database connection for walrcv_exec to
 	 * work.
 	 */
-	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
-	if (dbname == NULL)
-		ereport(ERROR,
+	*dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (*dbname == NULL)
+	{
+		ereport(LOG,
 
 		/*
 		 * translator: 'dbname' is a specific option; %s is a GUC variable
 		 * name
 		 */
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("exiting from slot synchronization due to bad configuration"),
+				errmsg("bad configuration for slot synchronization"),
 				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, sleep for MAX_WORKER_NAPTIME_MS and check again.
+ * The idea is to become no-op until we get valid GUCs values.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+	char	   *dbname;
+	int			rc;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	for (;;)
+	{
+		if (validate_parameters_and_get_dbname(&dbname))
+			break;
+
+		ereport(LOG, errmsg("skipping slot synchronization"));
+
+		ProcessSlotSyncInterrupts(NULL);
+
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   MAX_WORKER_NAPTIME_MS,
+					   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+		if (rc & WL_LATCH_SET)
+			ResetLatch(MyLatch);
+	}
 
 	return dbname;
 }
@@ -894,16 +962,28 @@ slotsync_reread_config(void)
 {
 	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
 	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_enable_syncslot = enable_syncslot;
 	bool		old_hot_standby_feedback = hot_standby_feedback;
 	bool		conninfo_changed;
 	bool		primary_slotname_changed;
 
+	Assert(enable_syncslot);
+
 	ConfigReloadPending = false;
 	ProcessConfigFile(PGC_SIGHUP);
 
 	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
 	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
 
+	if (old_enable_syncslot != enable_syncslot)
+	{
+		ereport(LOG,
+		/* translator: %s is a GUC variable name */
+				errmsg("slot sync worker will shutdown because"
+					   " %s is disabled", "enable_syncslot"));
+		proc_exit(0);
+	}
+
 	if (conninfo_changed ||
 		primary_slotname_changed ||
 		(old_hot_standby_feedback != hot_standby_feedback))
@@ -911,8 +991,7 @@ slotsync_reread_config(void)
 		ereport(LOG,
 				errmsg("slot sync worker will restart because of a parameter change"));
 
-		/* The exit code 1 will make postmaster restart this worker */
-		proc_exit(1);
+		proc_exit(0);
 	}
 
 	pfree(old_primary_conninfo);
@@ -929,7 +1008,8 @@ ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
 
 	if (ShutdownRequestPending)
 	{
-		walrcv_disconnect(wrconn);
+		if (wrconn)
+			walrcv_disconnect(wrconn);
 		ereport(LOG,
 				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
 		proc_exit(0);
@@ -961,17 +1041,16 @@ slotsync_worker_onexit(int code, Datum arg)
  * sync-cycles is reset to the minimum (200ms).
  */
 static void
-wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+wait_for_slot_activity(bool some_slot_updated, bool recheck_primary_info)
 {
 	int			rc;
 
-	if (am_cascading_standby)
+	if (recheck_primary_info)
 	{
 		/*
-		 * Slot synchronization is currently not supported on cascading
-		 * standby. So if we are on the cascading standby, we will skip the
-		 * sync and take a longer nap before we check again whether we are
-		 * still cascading standby or not.
+		 * If we are on the cascading standby or primary_slot_name configured
+		 * is not valid, then we will skip the sync and take a longer nap
+		 * before we can do check_primary_info() again.
 		 */
 		sleep_ms = MAX_WORKER_NAPTIME_MS;
 	}
@@ -1007,20 +1086,38 @@ wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
  * It connects to the primary server, fetches logical failover slots
  * information periodically in order to create and sync the slots.
  */
-void
-ReplSlotSyncWorkerMain(Datum main_arg)
+NON_EXEC_STATIC void
+ReplSlotSyncWorkerMain(int argc, char *argv[])
 {
 	WalReceiverConn *wrconn = NULL;
 	char	   *dbname;
 	bool		am_cascading_standby;
+	bool		primary_slot_invalid;
 	char	   *err;
+	sigjmp_buf	local_sigjmp_buf;
 
-	ereport(LOG, errmsg("replication slot sync worker started"));
+	am_slotsync_worker = true;
 
-	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+	MyBackendType = B_SLOTSYNC_WORKER;
 
-	SpinLockAcquire(&SlotSyncWorker->mutex);
+	init_ps_display(NULL);
+
+	SetProcessingMode(InitProcessing);
 
+	/*
+	 * Create a per-backend PGPROC struct in shared memory.  We must do this
+	 * before we access any shared memory.
+	 */
+	InitProcess();
+
+	/*
+	 * Early initialization.
+	 */
+	BaseInit();
+
+	Assert(SlotSyncWorker != NULL);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
 	Assert(SlotSyncWorker->pid == InvalidPid);
 
 	/*
@@ -1035,26 +1132,77 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 
 	/* Advertise our PID so that the startup process can kill us on promotion */
 	SlotSyncWorker->pid = MyProcPid;
-
 	SpinLockRelease(&SlotSyncWorker->mutex);
 
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
 	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
 	pqsignal(SIGTERM, die);
-	BackgroundWorkerUnblockSignals();
+	pqsignal(SIGFPE, FloatExceptionHandler);
+	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+	pqsignal(SIGUSR2, SIG_IGN);
+	pqsignal(SIGPIPE, SIG_IGN);
+	pqsignal(SIGCHLD, SIG_DFL);
+
+	/*
+	 * Establishes SIGALRM handler and initialize timeout module. It is needed
+	 * by InitPostgres to register different timeouts.
+	 */
+	InitializeTimeouts();
 
 	/* Load the libpq-specific functions */
 	load_file("libpqwalreceiver", false);
 
-	dbname = validate_parameters_and_get_dbname();
+	/*
+	 * If an exception is encountered, processing resumes here.
+	 *
+	 * We just need to clean up, report the error, and go away.
+	 *
+	 * If we do not have this handling here, then since this worker process
+	 * operates at the bottom of the exception stack, ERRORs turn into FATALs.
+	 * Therefore, we create our own exception handler to catch ERRORs.
+	 */
+	if (sigsetjmp(local_sigjmp_buf, 1) != 0)
+	{
+		/* since not using PG_TRY, must reset error stack by hand */
+		error_context_stack = NULL;
+
+		/* Prevents interrupts while cleaning up */
+		HOLD_INTERRUPTS();
+
+		/* Report the error to the server log */
+		EmitErrorReport();
+
+		/*
+		 * We can now go away.  Note that because we called InitProcess, a
+		 * callback was registered to do ProcKill, which will clean up
+		 * necessary state.
+		 */
+		proc_exit(0);
+	}
+
+	/* We can now handle ereport(ERROR) */
+	PG_exception_stack = &local_sigjmp_buf;
+
+	/*
+	 * Unblock signals (they were blocked when the postmaster forked us)
+	 */
+	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
+
+	dbname = wait_for_valid_params_and_get_dbname();
 
 	/*
 	 * Connect to the database specified by user in primary_conninfo. We need
 	 * a database connection for walrcv_exec to work. Please see comments atop
 	 * libpqrcv_exec.
 	 */
-	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+	InitPostgres(dbname, InvalidOid, NULL, InvalidOid, 0, NULL);
+
+	SetProcessingMode(NormalProcessing);
 
 	/*
 	 * Establish the connection to the primary server for slots
@@ -1073,26 +1221,29 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 	 * cascading standby and validates primary_slot_name for
 	 * non-cascading-standbys.
 	 */
-	check_primary_info(wrconn, &am_cascading_standby);
+	check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 
 	/* Main wait loop */
 	for (;;)
 	{
 		bool		some_slot_updated = false;
+		bool		recheck_primary_info = am_cascading_standby || primary_slot_invalid;
 
 		ProcessSlotSyncInterrupts(wrconn);
 
-		if (!am_cascading_standby)
+		if (!recheck_primary_info)
 			some_slot_updated = synchronize_slots(wrconn);
+		else if (primary_slot_invalid)
+			ereport(LOG, errmsg("skipping slot synchronization"));
 
-		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+		wait_for_slot_activity(some_slot_updated, recheck_primary_info);
 
 		/*
 		 * If the standby was promoted then what was previously a cascading
 		 * standby might no longer be one, so recheck each time.
 		 */
-		if (am_cascading_standby)
-			check_primary_info(wrconn, &am_cascading_standby);
+		if (recheck_primary_info)
+			check_primary_info(wrconn, &am_cascading_standby, &primary_slot_invalid);
 	}
 
 	/*
@@ -1108,7 +1259,7 @@ ReplSlotSyncWorkerMain(Datum main_arg)
 bool
 IsLogicalSlotSyncWorker(void)
 {
-	return SlotSyncWorker->pid == MyProcPid;
+	return am_slotsync_worker;
 }
 
 /*
@@ -1138,7 +1289,7 @@ ShutDownSlotSync(void)
 		/* Wait a bit, we don't expect to have to wait long */
 		rc = WaitLatch(MyLatch,
 					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
-					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+					   10L, WAIT_EVENT_REPL_SLOTSYNC_SHUTDOWN);
 
 		if (rc & WL_LATCH_SET)
 		{
@@ -1181,42 +1332,92 @@ SlotSyncWorkerShmemInit(void)
 	}
 }
 
+#ifdef EXEC_BACKEND
 /*
- * Register the background worker for slots synchronization provided
- * enable_syncslot is ON.
+ * The forkexec routine for the slot sync worker process.
+ *
+ * Format up the arglist, then fork and exec.
  */
-void
-SlotSyncWorkerRegister(void)
+static pid_t
+slotsyncworker_forkexec(void)
 {
-	BackgroundWorker bgw;
+	char	   *av[10];
+	int			ac = 0;
 
-	if (!enable_syncslot)
-	{
-		ereport(LOG,
-				errmsg("skipping slot synchronization"),
-				errdetail("\"enable_syncslot\" is disabled."));
-		return;
-	}
+	av[ac++] = "postgres";
+	av[ac++] = "--forkssworker";
+	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
+	av[ac] = NULL;
 
-	memset(&bgw, 0, sizeof(bgw));
+	Assert(ac < lengthof(av));
 
-	/* We need database connection which needs shared-memory access as well */
-	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
-		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	return postmaster_forkexec(ac, av);
+}
+#endif
 
-	/* Start as soon as a consistent state has been reached in a hot standby */
-	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+/*
+ * SlotSyncWorkerCanRestart
+ *
+ * Returns true if the worker is allowed to restart if enough time has
+ * passed (SLOTSYNC_RESTART_INTERVAL_SEC) since it was launched last.
+ * Otherwise returns false.
+ *
+ * This is a safety valve to protect against continuous respawn attempts if the
+ * worker is dying immediately at launch. Note that since we will retry to
+ * launch the worker from the postmaster main loop, we will get another
+ * chance later.
+ */
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
 
-	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
-	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
-	snprintf(bgw.bgw_name, BGW_MAXLEN,
-			 "replication slot sync worker");
-	snprintf(bgw.bgw_type, BGW_MAXLEN,
-			 "slot sync worker");
+	static time_t last_slotsync_start_time = 0;
+	time_t		curtime = time(NULL);
 
-	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
-	bgw.bgw_notify_pid = 0;
-	bgw.bgw_main_arg = (Datum) 0;
+	/* Return false if too soon since last start. */
+	if ((unsigned int) (curtime - last_slotsync_start_time) <
+		(unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC)
+		return false;
+
+	last_slotsync_start_time = curtime;
+	return true;
+}
+
+/*
+ * Main entry point for slot sync worker process, to be called from the
+ * postmaster.
+ */
+int
+StartSlotSyncWorker(void)
+{
+	pid_t		pid;
+
+#ifdef EXEC_BACKEND
+	switch ((pid = slotsyncworker_forkexec()))
+	{
+#else
+	switch ((pid = fork_process()))
+	{
+		case 0:
+			/* in postmaster child ... */
+			InitPostmasterChild();
+
+			/* Close the postmaster's sockets */
+			ClosePostmasterPorts(false);
+
+			ReplSlotSyncWorkerMain(0, NULL);
+			break;
+#endif
+		case -1:
+			ereport(LOG,
+					(errmsg("could not fork slot sync worker process: %m")));
+			return 0;
+
+		default:
+			return (int) pid;
+	}
 
-	RegisterBackgroundWorker(&bgw);
+	/* shouldn't get here */
+	return 0;
 }
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe..f19e5faeaf 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -42,6 +42,7 @@
 #include "replication/slot.h"
 #include "replication/syncrep.h"
 #include "replication/walsender.h"
+#include "replication/logicalworker.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -364,8 +365,12 @@ InitProcess(void)
 	 * child; this is so that the postmaster can detect it if we exit without
 	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
 	 * this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildActive();
 
 	/*
@@ -934,8 +939,12 @@ ProcKill(int code, Datum arg)
 	 * This process is no longer present in shared memory in any meaningful
 	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
 	 * autovac launcher should be included here someday)
+	 *
+	 * Slot sync worker is also not a postmaster child, so skip this shared
+	 * memory related processing here.
 	 */
-	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess())
+	if (IsUnderPostmaster && !IsAutoVacuumLauncherProcess() &&
+		!IsLogicalSlotSyncWorker())
 		MarkPostmasterChildInactive();
 
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e8c530acd9..1a34bd3715 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,17 +3286,6 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
-		else if (IsLogicalSlotSyncWorker())
-		{
-			elog(DEBUG1,
-				 "replication slot sync worker is shutting down due to administrator command");
-
-			/*
-			 * Slot sync worker can be stopped at any time. Use exit status 1
-			 * so the background worker is restarted.
-			 */
-			proc_exit(1);
-		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 43c393d6fe..9d6e067382 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -338,6 +338,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_BG_WORKER:
 		case B_BG_WRITER:
 		case B_CHECKPOINTER:
+		case B_SLOTSYNC_WORKER:
 		case B_STANDALONE_BACKEND:
 		case B_STARTUP:
 		case B_WAL_SENDER:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3e6203322a..4c0ee2dd29 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -54,6 +54,7 @@ LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher proc
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
 REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
+REPL_SLOTSYNC_SHUTDOWN	"Waiting for slot sync worker to shut down."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 23f77a59e5..309aa33a62 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -40,6 +40,7 @@
 #include "postmaster/interrupt.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/postmaster.h"
+#include "replication/logicalworker.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -293,6 +294,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case B_SLOTSYNC_WORKER:
+			backendDesc = "slotsyncworker";
+			break;
 		case B_STANDALONE_BACKEND:
 			backendDesc = "standalone backend";
 			break;
@@ -835,9 +839,10 @@ InitializeSessionUserIdStandalone(void)
 {
 	/*
 	 * This function should only be called in single-user mode, in autovacuum
-	 * workers, and in background workers.
+	 * workers, in slot sync worker and in background workers.
 	 */
-	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() || IsBackgroundWorker);
+	Assert(!IsUnderPostmaster || IsAutoVacuumWorkerProcess() ||
+		   IsLogicalSlotSyncWorker() || IsBackgroundWorker);
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad3367159..a5af0f410a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/postmaster.h"
 #include "replication/slot.h"
+#include "replication/logicalworker.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
@@ -874,10 +875,11 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 * Perform client authentication if necessary, then figure out our
 	 * postgres user ID, and see if we are a superuser.
 	 *
-	 * In standalone mode and in autovacuum worker processes, we use a fixed
-	 * ID, otherwise we figure it out from the authenticated user name.
+	 * In standalone mode, autovacuum worker processes and slot sync worker
+	 * process, we use a fixed ID, otherwise we figure it out from the
+	 * authenticated user name.
 	 */
-	if (bootstrap || IsAutoVacuumWorkerProcess())
+	if (bootstrap || IsAutoVacuumWorkerProcess() || IsLogicalSlotSyncWorker())
 	{
 		InitializeSessionUserIdStandalone();
 		am_superuser = true;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index fe044c16de..3af11b2b80 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2056,7 +2056,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 
 	{
-		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+		{"enable_syncslot", PGC_SIGHUP, REPLICATION_STANDBY,
 			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
 		},
 		&enable_syncslot,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f093..65819cb7a7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -332,6 +332,7 @@ typedef enum BackendType
 	B_BG_WRITER,
 	B_CHECKPOINTER,
 	B_LOGGER,
+	B_SLOTSYNC_WORKER,
 	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 7092fc72c6..22fc49ec27 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,7 +79,6 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
-	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 2167720971..a04815969b 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -329,9 +329,11 @@ extern void pa_decr_and_wait_stream_block(void);
 
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
-
-extern void ReplSlotSyncWorkerMain(Datum main_arg);
-extern void SlotSyncWorkerRegister(void);
+#ifdef EXEC_BACKEND
+extern void ReplSlotSyncWorkerMain(int argc, char *argv[]) pg_attribute_noreturn();
+#endif
+extern int	StartSlotSyncWorker(void);
+extern bool SlotSyncWorkerCanRestart(void);
 extern void ShutDownSlotSync(void);
 extern void SlotSyncWorkerShmemInit(void);
 
-- 
2.34.1



  [application/octet-stream] v74-0003-Allow-logical-walsenders-to-wait-for-the-physica.patch (40.2K, ../../CAJpy0uBG6JgFSM7Bt0-1D+6u1H9K2+C6wvBanfiWcK9TupHnFw@mail.gmail.com/3-v74-0003-Allow-logical-walsenders-to-wait-for-the-physica.patch)
  download | inline diff:
From 00f99a607c4c44579c5911186a50eef39a480310 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 30 Jan 2024 09:30:50 +0530
Subject: [PATCH v74 3/5] Allow logical walsenders to wait for the physical

This patch introduces a mechanism to ensure that physical standby servers,
which are potential failover candidates, have received and flushed changes
before making them visible to subscribers. By doing so, it guarantees that
the promoted standby server is not lagging behind the subscribers when a
failover is necessary.

A new parameter named standby_slot_names is introduced. The logical
walsender now guarantees that all local changes are sent and flushed to
the standby servers corresponding to the replication slots specified in
standby_slot_names before sending those changes to the subscriber.

Additionally, The SQL functions pg_logical_slot_get_changes and
pg_replication_slot_advance are modified to wait for the replication slots
mentioned in standby_slot_names to catch up before returning the changes
to the user.
---
 doc/src/sgml/config.sgml                      |  24 ++
 doc/src/sgml/logicaldecoding.sgml             |   8 +
 .../replication/logical/logicalfuncs.c        |  13 +
 src/backend/replication/logical/slotsync.c    |   1 +
 src/backend/replication/slot.c                | 342 +++++++++++++++++-
 src/backend/replication/slotfuncs.c           |   9 +
 src/backend/replication/walsender.c           | 111 +++++-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/misc/guc_tables.c           |  14 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/replication/slot.h                |   7 +
 src/include/replication/walsender.h           |   1 +
 src/include/replication/walsender_private.h   |   7 +
 src/include/utils/guc_hooks.h                 |   3 +
 src/test/recovery/t/006_logical_decoding.pl   |   3 +-
 .../t/040_standby_failover_slots_sync.pl      | 228 ++++++++++--
 16 files changed, 735 insertions(+), 39 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 75bb48b795..c1e34cb752 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4420,6 +4420,30 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-standby-slot-names" xreflabel="standby_slot_names">
+      <term><varname>standby_slot_names</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>standby_slot_names</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        List of physical slots guarantees that logical replication slots with
+        failover enabled do not consume changes until those changes are received
+        and flushed to corresponding physical standbys. If a logical replication
+        connection is meant to switch to a physical standby after the standby is
+        promoted, the physical replication slot for the standby should be listed
+        here.
+       </para>
+       <para>
+        The standbys corresponding to the physical replication slots in
+        <varname>standby_slot_names</varname> must configure
+        <literal>enable_syncslot = true</literal> so they can receive
+        failover logical slots changes from the primary.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index 9df5ff0145..bbec5860a6 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -376,6 +376,14 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
      <literal>dbname</literal> in the
      <link linkend="guc-primary-conninfo"><varname>primary_conninfo</varname></link>
      string, which is used for slot synchronization and is ignored for streaming.
+     It's also highly recommended that the said physical replication slot
+     is named in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     list on the primary, to prevent the subscriber from consuming changes
+     faster than the hot standby. But once we configure it, then certain latency
+     is expected in sending changes to logical subscribers due to wait on
+     physical replication slots in
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
     </para>
 
     <para>
diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c
index b0081d3ce5..5ff761dd65 100644
--- a/src/backend/replication/logical/logicalfuncs.c
+++ b/src/backend/replication/logical/logicalfuncs.c
@@ -30,6 +30,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -109,6 +110,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 	MemoryContext per_query_ctx;
 	MemoryContext oldcontext;
 	XLogRecPtr	end_of_wal;
+	XLogRecPtr	wait_for_wal_lsn;
 	LogicalDecodingContext *ctx;
 	ResourceOwner old_resowner = CurrentResourceOwner;
 	ArrayType  *arr;
@@ -228,6 +230,17 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin
 							NameStr(MyReplicationSlot->data.plugin),
 							format_procedure(fcinfo->flinfo->fn_oid))));
 
+		if (XLogRecPtrIsInvalid(upto_lsn))
+			wait_for_wal_lsn = end_of_wal;
+		else
+			wait_for_wal_lsn = Min(upto_lsn, end_of_wal);
+
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to wait_for_wal_lsn.
+		 */
+		WaitForStandbyConfirmation(wait_for_wal_lsn);
+
 		ctx->output_writer_private = p;
 
 		/*
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 76a73aa680..0dfc057ff9 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -44,6 +44,7 @@
 #include "commands/dbcommands.h"
 #include "libpq/pqsignal.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "postmaster/bgworker.h"
 #include "postmaster/fork_process.h"
 #include "postmaster/interrupt.h"
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 605dfb0426..273a38fafd 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,14 +46,19 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/walsender.h"
+#include "replication/walsender_private.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
 
 /*
  * Replication slot on-disk data structure.
@@ -100,10 +105,19 @@ ReplicationSlotCtlData *ReplicationSlotCtl = NULL;
 /* My backend's replication slot in the shared memory array */
 ReplicationSlot *MyReplicationSlot = NULL;
 
-/* GUC variable */
+/* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
+/*
+ * This GUC lists streaming replication standby server slot names that
+ * logical WAL sender processes will wait for.
+ */
+char	   *standby_slot_names;
+
+/* This is parsed and cached list for raw standby_slot_names. */
+static List *standby_slot_names_list = NIL;
+
 static void ReplicationSlotShmemExit(int code, Datum arg);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
@@ -2234,3 +2248,329 @@ RestoreSlotFromDisk(const char *name)
 				(errmsg("too many replication slots active before shutdown"),
 				 errhint("Increase max_replication_slots and try again.")));
 }
+
+/*
+ * A helper function to validate slots specified in GUC standby_slot_names.
+ */
+static bool
+validate_standby_slots(char **newval)
+{
+	char	   *rawname;
+	List	   *elemlist;
+	ListCell   *lc;
+	bool		ok;
+
+	/* Need a modifiable copy of string */
+	rawname = pstrdup(*newval);
+
+	/* Verify syntax and parse string into a list of identifiers */
+	ok = SplitIdentifierString(rawname, ',', &elemlist);
+
+	if (!ok)
+		GUC_check_errdetail("List syntax is invalid.");
+
+	/*
+	 * If there is a syntax error in the name or if the replication slots'
+	 * data is not initialized yet (i.e., we are in the startup process), skip
+	 * the slot verification.
+	 */
+	if (!ok || !ReplicationSlotCtl)
+	{
+		pfree(rawname);
+		list_free(elemlist);
+		return ok;
+	}
+
+	foreach(lc, elemlist)
+	{
+		char	   *name = lfirst(lc);
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			GUC_check_errdetail("replication slot \"%s\" does not exist",
+								name);
+			ok = false;
+			break;
+		}
+
+		if (!SlotIsPhysical(slot))
+		{
+			GUC_check_errdetail("\"%s\" is not a physical replication slot",
+								name);
+			ok = false;
+			break;
+		}
+	}
+
+	pfree(rawname);
+	list_free(elemlist);
+	return ok;
+}
+
+/*
+ * GUC check_hook for standby_slot_names
+ */
+bool
+check_standby_slot_names(char **newval, void **extra, GucSource source)
+{
+	if (strcmp(*newval, "") == 0)
+		return true;
+
+	/*
+	 * "*" is not accepted as in that case primary will not be able to know
+	 * for which all standbys to wait for. Even if we have physical-slots
+	 * info, there is no way to confirm whether there is any standby
+	 * configured for the known physical slots.
+	 */
+	if (strcmp(*newval, "*") == 0)
+	{
+		GUC_check_errdetail("\"%s\" is not accepted for standby_slot_names",
+							*newval);
+		return false;
+	}
+
+	/* Now verify if the specified slots really exist and have correct type */
+	if (!validate_standby_slots(newval))
+		return false;
+
+	*extra = guc_strdup(ERROR, *newval);
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for standby_slot_names
+ */
+void
+assign_standby_slot_names(const char *newval, void *extra)
+{
+	List	   *standby_slots;
+	MemoryContext oldcxt;
+	char	   *standby_slot_names_cpy = extra;
+
+	list_free(standby_slot_names_list);
+	standby_slot_names_list = NIL;
+
+	/* No value is specified for standby_slot_names. */
+	if (standby_slot_names_cpy == NULL)
+		return;
+
+	if (!SplitIdentifierString(standby_slot_names_cpy, ',', &standby_slots))
+	{
+		/* This should not happen if GUC checked check_standby_slot_names. */
+		elog(ERROR, "invalid list syntax");
+	}
+
+	/*
+	 * Switch to the same memory context under which GUC variables are
+	 * allocated (GUCMemoryContext).
+	 */
+	oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(standby_slot_names_cpy));
+	standby_slot_names_list = list_copy(standby_slots);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Return a copy of standby_slot_names_list if the copy flag is set to true,
+ * otherwise return the original list.
+ */
+List *
+GetStandbySlotList(bool copy)
+{
+	/*
+	 * Since we do not support syncing slots to cascading standbys, we return
+	 * NIL here if we are running in a standby to indicate that no standby
+	 * slots need to be waited for.
+	 */
+	if (RecoveryInProgress())
+		return NIL;
+
+	if (copy)
+		return list_copy(standby_slot_names_list);
+	else
+		return standby_slot_names_list;
+}
+
+/*
+ * Reload the config file and reinitialize the standby slot list if the GUC
+ * standby_slot_names has changed.
+ */
+void
+RereadConfigAndReInitSlotList(List **standby_slots)
+{
+	char	   *pre_standby_slot_names;
+
+	/*
+	 * If we are running on a standby, there is no need to reload
+	 * standby_slot_names since we do not support syncing slots to cascading
+	 * standbys.
+	 */
+	if (RecoveryInProgress())
+	{
+		ProcessConfigFile(PGC_SIGHUP);
+		return;
+	}
+
+	pre_standby_slot_names = pstrdup(standby_slot_names);
+
+	ProcessConfigFile(PGC_SIGHUP);
+
+	if (strcmp(pre_standby_slot_names, standby_slot_names) != 0)
+	{
+		list_free(*standby_slots);
+		*standby_slots = GetStandbySlotList(true);
+	}
+
+	pfree(pre_standby_slot_names);
+}
+
+/*
+ * Filter the standby slots based on the specified log sequence number
+ * (wait_for_lsn).
+ *
+ * This function updates the passed standby_slots list, removing any slots that
+ * have already caught up to or surpassed the given wait_for_lsn. Additionally,
+ * it removes slots that have been invalidated, dropped, or converted to
+ * logical slots.
+ */
+void
+FilterStandbySlots(XLogRecPtr wait_for_lsn, List **standby_slots)
+{
+	ListCell   *lc;
+	List	   *standby_slots_cpy = *standby_slots;
+
+	foreach(lc, standby_slots_cpy)
+	{
+		char	   *name = lfirst(lc);
+		char	   *warningfmt = NULL;
+		ReplicationSlot *slot;
+
+		slot = SearchNamedReplicationSlot(name, true);
+
+		if (!slot)
+		{
+			/*
+			 * It may happen that the slot specified in standby_slot_names GUC
+			 * value is dropped, so let's skip over it.
+			 */
+			warningfmt = _("replication slot \"%s\" specified in parameter \"%s\" does not exist, ignoring");
+		}
+		else if (SlotIsLogical(slot))
+		{
+			/*
+			 * If a logical slot name is provided in standby_slot_names, issue
+			 * a WARNING and skip it. Although logical slots are disallowed in
+			 * the GUC check_hook(validate_standby_slots), it is still
+			 * possible for a user to drop an existing physical slot and
+			 * recreate a logical slot with the same name. Since it is
+			 * harmless, a WARNING should be enough, no need to error-out.
+			 */
+			warningfmt = _("cannot have logical replication slot \"%s\" in parameter \"%s\", ignoring");
+		}
+		else
+		{
+			SpinLockAcquire(&slot->mutex);
+
+			if (slot->data.invalidated != RS_INVAL_NONE)
+			{
+				/*
+				 * Specified physical slot have been invalidated, so no point
+				 * in waiting for it.
+				 */
+				warningfmt = _("physical slot \"%s\" specified in parameter \"%s\" has been invalidated, ignoring");
+			}
+			else if (XLogRecPtrIsInvalid(slot->data.restart_lsn) ||
+					 slot->data.restart_lsn < wait_for_lsn)
+			{
+				bool		inactive = (slot->active_pid == 0);
+
+				SpinLockRelease(&slot->mutex);
+
+				/* Log warning if no active_pid for this physical slot */
+				if (inactive)
+					ereport(WARNING,
+							errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+								   name, "standby_slot_names"),
+							errdetail("Logical replication is waiting on the "
+									  "standby associated with \"%s\".", name),
+							errhint("Consider starting standby associated with "
+									"\"%s\" or amend standby_slot_names.", name));
+
+				/* Continue if the current slot hasn't caught up. */
+				continue;
+			}
+			else
+			{
+				Assert(slot->data.restart_lsn >= wait_for_lsn);
+			}
+
+			SpinLockRelease(&slot->mutex);
+		}
+
+		/*
+		 * Reaching here indicates that either the slot has passed the
+		 * wait_for_lsn or there is an issue with the slot that requires a
+		 * warning to be reported.
+		 */
+		if (warningfmt)
+			ereport(WARNING, errmsg(warningfmt, name, "standby_slot_names"));
+
+		standby_slots_cpy = foreach_delete_current(standby_slots_cpy, lc);
+	}
+
+	*standby_slots = standby_slots_cpy;
+}
+
+/*
+ * Wait for physical standby to confirm receiving the given lsn.
+ *
+ * Used by logical decoding SQL functions that acquired slot with failover
+ * enabled. It waits for physical standbys corresponding to the physical slots
+ * specified in the standby_slot_names GUC.
+ */
+void
+WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
+{
+	List	   *standby_slots;
+
+	if (!MyReplicationSlot->data.failover)
+		return;
+
+	standby_slots = GetStandbySlotList(true);
+
+	if (standby_slots == NIL)
+		return;
+
+	ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+
+	for (;;)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		if (ConfigReloadPending)
+		{
+			ConfigReloadPending = false;
+			RereadConfigAndReInitSlotList(&standby_slots);
+		}
+
+		FilterStandbySlots(wait_for_lsn, &standby_slots);
+
+		/* Exit if done waiting for every slot. */
+		if (standby_slots == NIL)
+			break;
+
+		/*
+		 * We wait for the slots in the standby_slot_names to catch up, but we
+		 * use a timeout so we can also check the if the standby_slot_names
+		 * has been changed.
+		 */
+		ConditionVariableTimedSleep(&WalSndCtl->wal_confirm_rcv_cv, 1000,
+									WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION);
+	}
+
+	ConditionVariableCancelSleep();
+	list_free(standby_slots);
+}
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 9913396152..983a544d75 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -21,6 +21,7 @@
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -474,6 +475,8 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto)
 		 * crash, but this makes the data consistent after a clean shutdown.
 		 */
 		ReplicationSlotMarkDirty();
+
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	return retlsn;
@@ -514,6 +517,12 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto)
 											   .segment_close = wal_segment_close),
 									NULL, NULL, NULL);
 
+		/*
+		 * Wait for specified streaming replication standby servers (if any)
+		 * to confirm receipt of WAL up to moveto lsn.
+		 */
+		WaitForStandbyConfirmation(moveto);
+
 		/*
 		 * Start reading at the slot's restart_lsn, which we know to point to
 		 * a valid record.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 15f045fe1f..1146887022 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1219,7 +1219,6 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
-
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
@@ -1728,27 +1727,78 @@ WalSndUpdateProgress(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId
 		ProcessPendingWrites();
 }
 
+/*
+ * Wake up the logical walsender processes with failover-enabled slots if the
+ * currently acquired physical slot is specified in standby_slot_names
+ * GUC.
+ */
+void
+PhysicalWakeupLogicalWalSnd(void)
+{
+	ListCell   *lc;
+	List	   *standby_slots;
+
+	Assert(MyReplicationSlot && SlotIsPhysical(MyReplicationSlot));
+
+	standby_slots = GetStandbySlotList(false);
+
+	foreach(lc, standby_slots)
+	{
+		char	   *name = lfirst(lc);
+
+		if (strcmp(name, NameStr(MyReplicationSlot->data.name)) == 0)
+		{
+			ConditionVariableBroadcast(&WalSndCtl->wal_confirm_rcv_cv);
+			return;
+		}
+	}
+}
+
 /*
  * Wait till WAL < loc is flushed to disk so it can be safely sent to client.
  *
- * Returns end LSN of flushed WAL.  Normally this will be >= loc, but
- * if we detect a shutdown request (either from postmaster or client)
- * we will return early, so caller must always check.
+ * If the walsender holds a logical slot that has enabled failover, we also
+ * wait for all the specified streaming replication standby servers to
+ * confirm receipt of WAL up to RecentFlushPtr.
+ *
+ * Returns end LSN of flushed WAL.  Normally this will be >= loc, but if we
+ * detect a shutdown request (either from postmaster or client) we will return
+ * early, so caller must always check.
  */
 static XLogRecPtr
 WalSndWaitForWal(XLogRecPtr loc)
 {
 	int			wakeEvents;
+	bool		wait_for_standby = false;
+	uint32		wait_event;
+	List	   *standby_slots = NIL;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
+	if (MyReplicationSlot->data.failover && replication_active)
+		standby_slots = GetStandbySlotList(true);
+
 	/*
-	 * Fast path to avoid acquiring the spinlock in case we already know we
-	 * have enough WAL available. This is particularly interesting if we're
-	 * far behind.
+	 * Check if all the standby servers have confirmed receipt of WAL up to
+	 * RecentFlushPtr even when we already know we have enough WAL available.
+	 *
+	 * Note that we cannot directly return without checking the status of
+	 * standby servers because the standby_slot_names may have changed, which
+	 * means there could be new standby slots in the list that have not yet
+	 * caught up to the RecentFlushPtr.
 	 */
-	if (RecentFlushPtr != InvalidXLogRecPtr &&
-		loc <= RecentFlushPtr)
-		return RecentFlushPtr;
+	if (!XLogRecPtrIsInvalid(RecentFlushPtr) && loc <= RecentFlushPtr)
+	{
+		FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
+		/*
+		 * Fast path to avoid acquiring the spinlock in case we already know
+		 * we have enough WAL available and all the standby servers have
+		 * confirmed receipt of WAL up to RecentFlushPtr. This is particularly
+		 * interesting if we're far behind.
+		 */
+		if (standby_slots == NIL)
+			return RecentFlushPtr;
+	}
 
 	/* Get a more recent flush pointer. */
 	if (!RecoveryInProgress())
@@ -1769,7 +1819,7 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (ConfigReloadPending)
 		{
 			ConfigReloadPending = false;
-			ProcessConfigFile(PGC_SIGHUP);
+			RereadConfigAndReInitSlotList(&standby_slots);
 			SyncRepInitConfig();
 		}
 
@@ -1784,8 +1834,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (got_STOPPING)
 			XLogBackgroundFlush();
 
+		/*
+		 * Update the standby slots that have not yet caught up to the flushed
+		 * position. It is good to wait up to RecentFlushPtr and then let it
+		 * send the changes to logical subscribers one by one which are
+		 * already covered in RecentFlushPtr without needing to wait on every
+		 * change for standby confirmation.
+		 */
+		if (wait_for_standby)
+			FilterStandbySlots(RecentFlushPtr, &standby_slots);
+
 		/* Update our idea of the currently flushed position. */
-		if (!RecoveryInProgress())
+		else if (!RecoveryInProgress())
 			RecentFlushPtr = GetFlushRecPtr(NULL);
 		else
 			RecentFlushPtr = GetXLogReplayRecPtr(NULL);
@@ -1813,9 +1873,18 @@ WalSndWaitForWal(XLogRecPtr loc)
 			!waiting_for_ping_response)
 			WalSndKeepalive(false, InvalidXLogRecPtr);
 
-		/* check whether we're done */
-		if (loc <= RecentFlushPtr)
+		if (loc > RecentFlushPtr)
+			wait_event = WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL;
+		else if (standby_slots)
+		{
+			wait_event = WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION;
+			wait_for_standby = true;
+		}
+		else
+		{
+			/* Already caught up and doesn't need to wait for standby_slots. */
 			break;
+		}
 
 		/* Waiting for new WAL. Since we need to wait, we're now caught up. */
 		WalSndCaughtUp = true;
@@ -1855,9 +1924,11 @@ WalSndWaitForWal(XLogRecPtr loc)
 		if (pq_is_send_pending())
 			wakeEvents |= WL_SOCKET_WRITEABLE;
 
-		WalSndWait(wakeEvents, sleeptime, WAIT_EVENT_WAL_SENDER_WAIT_FOR_WAL);
+		WalSndWait(wakeEvents, sleeptime, wait_event);
 	}
 
+	list_free(standby_slots);
+
 	/* reactivate latch so WalSndLoop knows to continue */
 	SetLatch(MyLatch);
 	return RecentFlushPtr;
@@ -2265,6 +2336,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredLSN();
+		PhysicalWakeupLogicalWalSnd();
 	}
 
 	/*
@@ -3532,6 +3604,7 @@ WalSndShmemInit(void)
 
 		ConditionVariableInit(&WalSndCtl->wal_flush_cv);
 		ConditionVariableInit(&WalSndCtl->wal_replay_cv);
+		ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv);
 	}
 }
 
@@ -3601,8 +3674,14 @@ WalSndWait(uint32 socket_events, long timeout, uint32 wait_event)
 	 *
 	 * And, we use separate shared memory CVs for physical and logical
 	 * walsenders for selective wake ups, see WalSndWakeup() for more details.
+	 *
+	 * If the wait event is WAIT_FOR_STANDBY_CONFIRMATION, wait on another CV
+	 * until awakened by physical walsenders after the walreceiver confirms
+	 * the receipt of the LSN.
 	 */
-	if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
+	if (wait_event == WAIT_EVENT_WAIT_FOR_STANDBY_CONFIRMATION)
+		ConditionVariablePrepareToSleep(&WalSndCtl->wal_confirm_rcv_cv);
+	else if (MyWalSnd->kind == REPLICATION_KIND_PHYSICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv);
 	else if (MyWalSnd->kind == REPLICATION_KIND_LOGICAL)
 		ConditionVariablePrepareToSleep(&WalSndCtl->wal_replay_cv);
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4c0ee2dd29..9973ef41b9 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -78,6 +78,7 @@ GSS_OPEN_SERVER	"Waiting to read data from the client while establishing a GSSAP
 LIBPQWALRECEIVER_CONNECT	"Waiting in WAL receiver to establish connection to remote server."
 LIBPQWALRECEIVER_RECEIVE	"Waiting in WAL receiver to receive data from remote server."
 SSL_OPEN_SERVER	"Waiting for SSL while attempting connection."
+WAIT_FOR_STANDBY_CONFIRMATION	"Waiting for the WAL to be received by physical standby."
 WAL_SENDER_WAIT_FOR_WAL	"Waiting for WAL to be flushed in WAL sender process."
 WAL_SENDER_WRITE_DATA	"Waiting for any activity when processing replies from WAL receiver in WAL sender process."
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3af11b2b80..a82618c3d4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4628,6 +4628,20 @@ struct config_string ConfigureNamesString[] =
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
 
+	{
+		{"standby_slot_names", PGC_SIGHUP, REPLICATION_PRIMARY,
+			gettext_noop("Lists streaming replication standby server slot "
+						 "names that logical WAL sender processes will wait for."),
+			gettext_noop("Decoded changes are sent out to plugins by logical "
+						 "WAL sender processes only after specified "
+						 "replication slots confirm receiving WAL."),
+			GUC_LIST_INPUT | GUC_LIST_QUOTE
+		},
+		&standby_slot_names,
+		"",
+		check_standby_slot_names, assign_standby_slot_names, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3868694d3f..db4afaf356 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -334,6 +334,8 @@
 				# method to choose sync standbys, number of sync standbys,
 				# and comma-separated list of application_name
 				# from standby(s); '*' = all
+#standby_slot_names = '' # streaming replication standby server slot names that
+				# logical walsender processes will wait for
 
 # - Standby Servers -
 
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 1f4446aa3a..df56e1271e 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -229,6 +229,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT char *standby_slot_names;
 
 /* shmem initialization functions */
 extern Size ReplicationSlotsShmemSize(void);
@@ -275,4 +276,10 @@ extern void CheckPointReplicationSlots(bool is_shutdown);
 extern void CheckSlotRequirements(void);
 extern void CheckSlotPermissions(void);
 
+extern List *GetStandbySlotList(bool copy);
+extern void WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn);
+extern void FilterStandbySlots(XLogRecPtr wait_for_lsn,
+							   List **standby_slots);
+extern void RereadConfigAndReInitSlotList(List **standby_slots);
+
 #endif							/* SLOT_H */
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 276d8913aa..f9a559f831 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -47,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern void PhysicalWakeupLogicalWalSnd(void);
 extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h
index 3113e9ea47..0f962b0c72 100644
--- a/src/include/replication/walsender_private.h
+++ b/src/include/replication/walsender_private.h
@@ -113,6 +113,13 @@ typedef struct
 	ConditionVariable wal_flush_cv;
 	ConditionVariable wal_replay_cv;
 
+	/*
+	 * Used by physical walsenders holding slots specified in
+	 * standby_slot_names to wake up logical walsenders holding
+	 * failover-enabled slots when a walreceiver confirms the receipt of LSN.
+	 */
+	ConditionVariable wal_confirm_rcv_cv;
+
 	WalSnd		walsnds[FLEXIBLE_ARRAY_MEMBER];
 } WalSndCtlData;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b..464996b4f0 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -162,5 +162,8 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_wal_sync_method(int new_wal_sync_method, void *extra);
+extern bool check_standby_slot_names(char **newval, void **extra,
+									 GucSource source);
+extern void assign_standby_slot_names(const char *newval, void *extra);
 
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 5c7b4ca5e3..85f019774c 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -172,9 +172,10 @@ is($node_primary->slot('otherdb_slot')->{'slot_name'},
 	undef, 'logical slot was actually dropped with DB');
 
 # Test logical slot advancing and its durability.
+# Pass failover=true (last-arg), it should not have any impact on advancing.
 my $logical_slot = 'logical_slot';
 $node_primary->safe_psql('postgres',
-	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false);"
+	"SELECT pg_create_logical_replication_slot('$logical_slot', 'test_decoding', false, false, true);"
 );
 $node_primary->psql(
 	'postgres', "
diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl
index 2a017b4286..79c46ef4d7 100644
--- a/src/test/recovery/t/040_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl
@@ -99,17 +99,30 @@ ok( $stderr =~ /ERROR:  cannot set failover for enabled subscription/,
 	"altering failover is not allowed for enabled subscription");
 
 ##################################################
-# Test logical failover slots on the standby
-# Configure standby1 to replicate and synchronize logical slots configured
-# for failover on the primary
+# Test primary disallowing specified logical replication slots getting ahead of
+# specified physical replication slots. It uses the following set up:
 #
-#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
-# primary --->                           |
-#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
-#                                        |                 lsub1_slot(synced_slot)
+#				| ----> standby1 (primary_slot_name = sb1_slot)
+#				| ----> standby2 (primary_slot_name = sb2_slot)
+# primary -----	|
+#				| ----> subscriber1 (failover = true)
+#				| ----> subscriber2 (failover = false)
+#
+# standby_slot_names = 'sb1_slot'
+#
+# Set up is configured in such a way that the logical slot of subscriber1 is
+# enabled failover, thus it will wait for the physical slot of
+# standby1(sb1_slot) to catch up before sending decoded changes to subscriber1.
 ##################################################
 
+# Create primary
 my $primary = $publisher;
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb2_slot');});
+
 my $backup_name = 'backup';
 $primary->backup($backup_name);
 
@@ -119,21 +132,201 @@ $standby1->init_from_backup(
 	$primary, $backup_name,
 	has_streaming => 1,
 	has_restoring => 1);
+$standby1->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb1_slot'
+));
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
 
+# Create another standby
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+$standby2->append_conf(
+	'postgresql.conf', qq(
+primary_slot_name = 'sb2_slot'
+));
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+# Configure primary to disallow any logical slots that enabled failover from
+# getting ahead of specified physical replication slot (sb1_slot).
+$primary->append_conf(
+	'postgresql.conf', qq(
+standby_slot_names = 'sb1_slot'
+));
+$primary->reload;
+
+$primary->safe_psql('postgres', "CREATE TABLE tab_int (a int PRIMARY KEY);");
+
+# Create a table and refresh the publication
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION WITH (copy_data = false);
+]);
+
+# Create another subscriber node without enabling failover, wait for sync to
+# complete
+my $subscriber2 = PostgreSQL::Test::Cluster->new('subscriber2');
+$subscriber2->init;
+$subscriber2->start;
+$subscriber2->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	CREATE SUBSCRIPTION regress_mysub2 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub2_slot, copy_data = false);
+]);
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# comes up.
+$standby1->stop;
+
+# Create some data on the primary
+my $primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+# Wait for the standby that's up and running gets the data from primary
+$primary->wait_for_replay_catchup($standby2);
+$result = $standby2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby2 gets data from primary");
+
+# Wait for the subscription that's up and running and is not enabled for failover.
+# It gets the data from primary without waiting for any standbys.
+$publisher->wait_for_catchup('regress_mysub2');
+$result = $subscriber2->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "subscriber2 gets data from primary");
+
+# The subscription that's up and running and is enabled for failover
+# doesn't get the data from primary and keeps waiting for the
+# standby specified in standby_slot_names.
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data from primary until standby1 acknowledges changes"
+);
+
+# Start the standby specified in standby_slot_names and wait for it to catch
+# up with the primary.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+$result = $standby1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't', "standby1 gets data from primary");
+
+# Now that the standby specified in standby_slot_names is up and running,
+# primary must send the decoded changes to subscription enabled for failover
+# While the standby was down, this subscriber didn't receive any data from
+# primary i.e. the primary didn't allow it to go ahead of standby.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 acknowledges changes");
+
+# Stop the standby associated with the specified physical replication slot so
+# that the logical replication slot won't receive changes until the standby
+# slot's restart_lsn is advanced or the slot is removed from the
+# standby_slot_names list.
+$publisher->safe_psql('postgres', "TRUNCATE tab_int;");
+$publisher->wait_for_catchup('regress_mysub1');
+$standby1->stop;
+
+##################################################
+# Verify that when using pg_logical_slot_get_changes to consume changes from a
+# logical slot with failover enabled, it will also wait for the slots specified
+# in standby_slot_names to catch up.
+##################################################
+
+# Create a logical 'test_decoding' replication slot with failover enabled
+$publisher->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
+);
+
+my $back_q = $primary->background_psql('postgres', on_error_stop => 0);
+my $pid = $back_q->query('SELECT pg_backend_pid()');
+
+# Try and get changes from the logical slot with failover enabled.
+my $offset = -s $primary->logfile;
+$back_q->query_until(qr//,
+	"SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);\n");
+
+# Wait until the primary server logs a warning indicating that it is waiting
+# for the sb1_slot to catch up.
+$primary->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? replication slot \"sb1_slot\" specified in parameter \"standby_slot_names\" does not have active_pid/,
+	$offset);
+
+ok($primary->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"),
+	"cancelling pg_logical_slot_get_changes command");
+
+$back_q->quit;
+
+$publisher->safe_psql('postgres',
+	"SELECT pg_drop_replication_slot('test_slot');"
+);
+
+##################################################
+# Test that logical replication will wait for the user-created inactive
+# physical slot to catch up until we remove the slot from standby_slot_names.
+##################################################
+
+# Create some data on the primary
+$primary_row_count = 10;
+$primary->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(1, $primary_row_count);");
+
+$result =
+  $subscriber1->safe_psql('postgres', "SELECT count(*) = 0 FROM tab_int;");
+is($result, 't',
+	"subscriber1 doesn't get data as the sb1_slot doesn't catch up");
+
+# Remove the standby from the standby_slot_names list and reload the
+# configuration.
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', "''");
+$primary->reload;
+
+# Since there are no slots in standby_slot_names, the primary server should now
+# send the decoded changes to the subscription.
+$publisher->wait_for_catchup('regress_mysub1');
+$result = $subscriber1->safe_psql('postgres',
+	"SELECT count(*) = $primary_row_count FROM tab_int;");
+is($result, 't',
+	"subscriber1 gets data from primary after standby1 is removed from the standby_slot_names list"
+);
+
+# Put the standby back on the primary_slot_name for the rest of the tests
+$primary->adjust_conf('postgresql.conf', 'standby_slot_names', 'sb1_slot');
+$primary->reload;
+
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+# Create a standby
 my $connstr_1 = $primary->connstr;
 $standby1->append_conf(
 	'postgresql.conf', qq(
 enable_syncslot = true
 hot_standby_feedback = on
-primary_slot_name = 'sb1_slot'
 primary_conninfo = '$connstr_1 dbname=postgres'
 ));
 
-$primary->psql('postgres',
-	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
-
 my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
-my $offset = -s $standby1->logfile;
+$offset = -s $standby1->logfile;
 
 # Start the standby so that slot syncing can begin
 $standby1->start;
@@ -162,18 +355,11 @@ is($standby1->safe_psql('postgres',
 # Insert data on the primary
 $primary->safe_psql(
 	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
+	TRUNCATE TABLE tab_int;
 	INSERT INTO tab_int SELECT generate_series(1, 10);
 ]);
 
-# Subscribe to the new table data and wait for it to arrive
-$subscriber1->safe_psql(
-	'postgres', qq[
-	CREATE TABLE tab_int (a int PRIMARY KEY);
-	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
-]);
-
-$subscriber1->wait_for_subscription_sync;
+$primary->wait_for_catchup('regress_mysub1');
 
 # Do not allow any further advancement of the restart_lsn and
 # confirmed_flush_lsn for the lsub1_slot.
-- 
2.34.1



  [application/octet-stream] v74-0005-Document-the-steps-to-check-if-the-standby-is-re.patch (6.6K, ../../CAJpy0uBG6JgFSM7Bt0-1D+6u1H9K2+C6wvBanfiWcK9TupHnFw@mail.gmail.com/4-v74-0005-Document-the-steps-to-check-if-the-standby-is-re.patch)
  download | inline diff:
From 333e320ff0a3890825e34ba240655d2679def23b Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 19 Jan 2024 11:04:16 +0530
Subject: [PATCH v74 5/5] Document the steps to check if the standby is ready
 for failover

---
 doc/src/sgml/high-availability.sgml   |   9 ++
 doc/src/sgml/logical-replication.sgml | 130 ++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..36215aa68c 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -1487,6 +1487,15 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
     Written administration procedures are advised.
    </para>
 
+   <para>
+    If you have opted for synchronization of logical slots (see
+    <xref linkend="logicaldecoding-replication-slots-synchronization"/>),
+    then before switching to the standby server, it is recommended to check
+    if the logical slots synchronized on the standby server are ready
+    for failover. This can be done by following the steps described in
+    <xref linkend="logical-replication-failover"/>.
+   </para>
+
    <para>
     To trigger failover of a log-shipping standby server, run
     <command>pg_ctl promote</command> or call <function>pg_promote()</function>.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ec2130669e..924e4ea033 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -687,6 +687,136 @@ ALTER SUBSCRIPTION
 
  </sect1>
 
+ <sect1 id="logical-replication-failover">
+  <title>Logical Replication Failover</title>
+
+  <para>
+   When the publisher server is the primary server of a streaming replication,
+   the logical slots on that primary server can be synchronized to the standby
+   server by specifying <literal>failover = true</literal> when creating
+   subscriptions for those publications. Enabling failover ensures a seamless
+   transition of those subscriptions after the standby is promoted. They can
+   continue subscribing to publications now on the new primary server without
+   any data loss.
+  </para>
+
+  <para>
+   Because the slot synchronization logic copies asynchronously, it is
+   necessary to confirm that replication slots have been synced to the standby
+   server before the failover happens. Furthermore, to ensure a successful
+   failover, the standby server must not be lagging behind the subscriber. It
+   is highly recommended to use
+   <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+   to prevent the subscriber from consuming changes faster than the hot standby.
+   To confirm that the standby server is indeed ready for failover, follow
+   these 2 steps:
+  </para>
+
+  <procedure>
+   <step performance="required">
+    <para>
+     Confirm that all the necessary logical replication slots have been synced to
+     the standby server.
+    </para>
+    <substeps>
+     <step performance="required">
+      <para>
+       Firstly, on the subscriber node, use the following SQL to identify
+       which slots should be synced to the standby that we plan to promote.
+<programlisting>
+test_sub=# SELECT
+               array_agg(slotname) AS slots
+           FROM
+           ((
+               SELECT r.srsubid AS subid, CONCAT('pg_' || srsubid || '_sync_' || srrelid || '_' || ctl.system_identifier) AS slotname
+               FROM pg_control_system() ctl, pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate = 'f' AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT s.oid AS subid, s.subslotname as slotname
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ slots
+-------
+ {sub1,sub2,sub3}
+(1 row)
+</programlisting></para>
+     </step>
+     <step performance="required">
+      <para>
+       Next, check that the logical replication slots identified above exist on
+       the standby server and are ready for failover.
+<programlisting>
+test_standby=# SELECT slot_name, (synced AND NOT temporary AND conflict_reason IS NULL) AS failover_ready
+               FROM pg_replication_slots
+               WHERE slot_name IN ('sub1','sub2','sub3');
+  slot_name  | failover_ready
+-------------+----------------
+  sub1       | t
+  sub2       | t
+  sub3       | t
+(3 rows)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+
+   <step performance="required">
+    <para>
+     Confirm that the standby server is not lagging behind the subscribers.
+     This step can be skipped if
+     <link linkend="guc-standby-slot-names"><varname>standby_slot_names</varname></link>
+     has been correctly configured.
+    </para>
+     <substeps>
+      <step performance="required">
+       <para>
+        Firstly, on the subscriber node check the last replayed WAL.
+<programlisting>
+test_sub=# SELECT
+               MAX(remote_lsn) AS remote_lsn_on_subscriber
+           FROM
+           ((
+               SELECT (CASE WHEN r.srsubstate = 'f' THEN pg_replication_origin_progress(CONCAT('pg_' || r.srsubid || '_' || r.srrelid), false)
+                           WHEN r.srsubstate IN ('s', 'r') THEN r.srsublsn END) AS remote_lsn
+               FROM pg_subscription_rel r, pg_subscription s
+               WHERE r.srsubstate IN ('f', 's', 'r') AND s.oid = r.srsubid AND s.subfailover
+           ) UNION (
+               SELECT pg_replication_origin_progress(CONCAT('pg_' || s.oid), false) AS remote_lsn
+               FROM pg_subscription s
+               WHERE s.subfailover
+           ));
+ remote_lsn_on_subscriber
+--------------------------
+ 0/3000388
+</programlisting></para>
+    </step>
+    <step performance="required">
+     <para>
+      Next, on the standby server check that the last-received WAL location
+      is ahead of the replayed WAL location on the subscriber identified above.
+      If the above SQL result was NULL, it means the subscriber has not yet
+      replayed any WAL, so the standby server must be ahead of the
+      subscriber, and this step can be skipped.
+<programlisting>
+test_standby=# SELECT pg_last_wal_receive_lsn() >= '0/3000388'::pg_lsn AS failover_ready;
+ failover_ready
+----------------
+ t
+(1 row)
+</programlisting></para>
+     </step>
+    </substeps>
+   </step>
+  </procedure>
+
+  <para>
+   If the result (<literal>failover_ready</literal>) of both above steps is
+   true, existing subscriptions will be able to continue without data loss.
+  </para>
+
+ </sect1>
+
  <sect1 id="logical-replication-row-filter">
   <title>Row Filters</title>
 
-- 
2.34.1



  [application/octet-stream] v74-0004-Non-replication-connection-and-app_name-change.patch (9.4K, ../../CAJpy0uBG6JgFSM7Bt0-1D+6u1H9K2+C6wvBanfiWcK9TupHnFw@mail.gmail.com/5-v74-0004-Non-replication-connection-and-app_name-change.patch)
  download | inline diff:
From 40a1e3f934eaa1cec6cb5c211133903ac0d28e6f Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 23 Jan 2024 15:48:53 +0530
Subject: [PATCH v74 4/5] Non replication connection and app_name change.

This patch has converted replication connection to non-replication
one in the slotsync worker.

It has also changed the app_name to {cluster_name}_slotsyncworker
in the slotsync worker connection.
---
 src/backend/commands/subscriptioncmds.c       |  8 ++--
 .../libpqwalreceiver/libpqwalreceiver.c       | 44 +++++++++++++------
 src/backend/replication/logical/slotsync.c    | 13 +++++-
 src/backend/replication/logical/tablesync.c   |  2 +-
 src/backend/replication/logical/worker.c      |  2 +-
 src/backend/replication/walreceiver.c         |  2 +-
 src/include/replication/walreceiver.h         |  5 ++-
 7 files changed, 52 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index b647a81fc8..a400ba0e40 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -759,7 +759,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = !superuser_arg(owner) && opts.passwordrequired;
-		wrconn = walrcv_connect(conninfo, true, must_use_password,
+		wrconn = walrcv_connect(conninfo, true, true, must_use_password,
 								stmt->subname, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -910,7 +910,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 	/* Try to connect to the publisher. */
 	must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-	wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+	wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password,
 							sub->name, &err);
 	if (!wrconn)
 		ereport(ERROR,
@@ -1537,7 +1537,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 		/* Try to connect to the publisher. */
 		must_use_password = sub->passwordrequired && !sub->ownersuperuser;
-		wrconn = walrcv_connect(sub->conninfo, true, must_use_password,
+		wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password,
 								sub->name, &err);
 		if (!wrconn)
 			ereport(ERROR,
@@ -1788,7 +1788,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	 */
 	load_file("libpqwalreceiver", false);
 
-	wrconn = walrcv_connect(conninfo, true, must_use_password,
+	wrconn = walrcv_connect(conninfo, true, true, must_use_password,
 							subname, &err);
 	if (wrconn == NULL)
 	{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 61eeaa17b2..512ec2897a 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -6,6 +6,9 @@
  * loaded as a dynamic module to avoid linking the main server binary with
  * libpq.
  *
+ * Apart from walreceiver, the libpq-specific routines here are now being used
+ * by logical replication workers and slotsync worker as well.
+
  * Portions Copyright (c) 2010-2024, PostgreSQL Global Development Group
  *
  *
@@ -49,7 +52,8 @@ struct WalReceiverConn
 
 /* Prototypes for interface functions */
 static WalReceiverConn *libpqrcv_connect(const char *conninfo,
-										 bool logical, bool must_use_password,
+										 bool replication, bool logical,
+										 bool must_use_password,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
@@ -124,7 +128,12 @@ _PG_init(void)
 }
 
 /*
- * Establish the connection to the primary server for XLOG streaming
+ * Establish the connection to the primary server.
+ *
+ * The connection established could be either a replication one or
+ * a non-replication one based on input argument 'replication'. And further
+ * if it is a replication connection, it could be either logical or physical
+ * based on input argument 'logical'.
  *
  * If an error occurs, this function will normally return NULL and set *err
  * to a palloc'ed error message. However, if must_use_password is true and
@@ -135,8 +144,8 @@ _PG_init(void)
  * case.
  */
 static WalReceiverConn *
-libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
-				 const char *appname, char **err)
+libpqrcv_connect(const char *conninfo, bool replication, bool logical,
+				 bool must_use_password, const char *appname, char **err)
 {
 	WalReceiverConn *conn;
 	PostgresPollingStatusType status;
@@ -159,17 +168,26 @@ libpqrcv_connect(const char *conninfo, bool logical, bool must_use_password,
 	 */
 	keys[i] = "dbname";
 	vals[i] = conninfo;
-	keys[++i] = "replication";
-	vals[i] = logical ? "database" : "true";
-	if (!logical)
+
+	/* We can not have logical without replication */
+	if (!replication)
+		Assert(!logical);
+	else
 	{
-		/*
-		 * The database name is ignored by the server in replication mode, but
-		 * specify "replication" for .pgpass lookup.
-		 */
-		keys[++i] = "dbname";
-		vals[i] = "replication";
+		keys[++i] = "replication";
+		vals[i] = logical ? "database" : "true";
+
+		if (!logical)
+		{
+			/*
+			 * The database name is ignored by the server in replication mode,
+			 * but specify "replication" for .pgpass lookup.
+			 */
+			keys[++i] = "dbname";
+			vals[i] = "replication";
+		}
 	}
+
 	keys[++i] = "fallback_application_name";
 	vals[i] = appname;
 	if (logical)
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 0dfc057ff9..c6f02dae8d 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -1096,6 +1096,7 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 	bool		primary_slot_invalid;
 	char	   *err;
 	sigjmp_buf	local_sigjmp_buf;
+	StringInfoData app_name;
 
 	am_slotsync_worker = true;
 
@@ -1205,13 +1206,21 @@ ReplSlotSyncWorkerMain(int argc, char *argv[])
 
 	SetProcessingMode(NormalProcessing);
 
+	initStringInfo(&app_name);
+	if (cluster_name[0])
+		appendStringInfo(&app_name, "%s_%s", cluster_name, "slotsyncworker");
+	else
+		appendStringInfo(&app_name, "%s", "slotsyncworker");
+
 	/*
 	 * Establish the connection to the primary server for slots
 	 * synchronization.
 	 */
-	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
-							cluster_name[0] ? cluster_name : "slotsyncworker",
+	wrconn = walrcv_connect(PrimaryConnInfo, false, false, false,
+							app_name.data,
 							&err);
+	pfree(app_name.data);
+
 	if (!wrconn)
 		ereport(ERROR,
 				errcode(ERRCODE_CONNECTION_FAILURE),
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 5acab3f3e2..ee06629088 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1329,7 +1329,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	 * so that synchronous replication can distinguish them.
 	 */
 	LogRepWorkerWalRcvConn =
-		walrcv_connect(MySubscription->conninfo, true,
+		walrcv_connect(MySubscription->conninfo, true, true,
 					   must_use_password,
 					   slotname, &err);
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 32ff4c0336..9dd2446fbf 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4519,7 +4519,7 @@ run_apply_worker()
 		!MySubscription->ownersuperuser;
 
 	LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true,
-											must_use_password,
+											true, must_use_password,
 											MySubscription->name, &err);
 
 	if (LogRepWorkerWalRcvConn == NULL)
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index e29a6196a3..b80447d15f 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -296,7 +296,7 @@ WalReceiverMain(void)
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
 	/* Establish the connection to the primary for XLOG streaming */
-	wrconn = walrcv_connect(conninfo, false, false,
+	wrconn = walrcv_connect(conninfo, true, false, false,
 							cluster_name[0] ? cluster_name : "walreceiver",
 							&err);
 	if (!wrconn)
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 48dc846e19..1b563a16cd 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -237,6 +237,7 @@ typedef struct WalRcvExecResult
  * returned with 'err' including the error generated.
  */
 typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
+											   bool replication,
 											   bool logical,
 											   bool must_use_password,
 											   const char *appname,
@@ -426,8 +427,8 @@ typedef struct WalReceiverFunctionsType
 
 extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 
-#define walrcv_connect(conninfo, logical, must_use_password, appname, err) \
-	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
+#define walrcv_connect(conninfo, replication, logical, must_use_password, appname, err) \
+	WalReceiverFunctions->walrcv_connect(conninfo, replication, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
 #define walrcv_get_conninfo(conn) \
-- 
2.34.1



  [application/octet-stream] v74-0001-Add-logical-slot-sync-capability-to-the-physical.patch (91.8K, ../../CAJpy0uBG6JgFSM7Bt0-1D+6u1H9K2+C6wvBanfiWcK9TupHnFw@mail.gmail.com/6-v74-0001-Add-logical-slot-sync-capability-to-the-physical.patch)
  download | inline diff:
From 8eb58b40971e1bd4a6089b1bf9f5b2651b5dc1ae Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Tue, 30 Jan 2024 09:20:46 +0530
Subject: [PATCH v74 1/5] Add logical slot sync capability to the physical
 standby

This patch implements synchronization of logical replication slots
from the primary server to the physical standby so that logical
replication can be resumed after failover.

GUC 'enable_syncslot' enables a physical standby to synchronize failover
logical replication slots from the primary server.

The logical replication slots on the primary can be synchronized to the hot
standby by enabling the failover option during slot creation and setting
'enable_syncslot' on the standby. For the synchronization to work, it is
mandatory to have a physical replication slot between the primary and the
standby, and hot_standby_feedback must be enabled on the standby.

All the failover logical replication slots on the primary (assuming
configurations are appropriate) are automatically created on the physical
standbys and are synced periodically. Slot-sync worker on the standby server
ping the primary server at regular intervals to get the necessary
failover logical slots information and create/update the slots locally.

The nap time of the worker is tuned according to the activity on the primary.
The worker waits for a period of time before the next synchronization, with the
duration varying based on whether any slots were updated during the last
cycle.

The logical slots created by slot-sync worker on physical standbys are not
allowed to be dropped or consumed. Any attempt to perform logical decoding on
such slots will result in an error.

If a logical slot is invalidated on the primary, then that slot on the standby
is also invalidated.

If a logical slot on the primary is valid but is invalidated on the standby,
then that slot is dropped and recreated on the standby in next sync-cycle
provided the slot still exists on the primary server. It is okay to recreate
such slots as long as these are not consumable on the standby (which is the
case currently). This situation may occur due to the following reasons:
- The max_slot_wal_keep_size on the standby is insufficient to retain WAL
  records from the restart_lsn of the slot.
- primary_slot_name is temporarily reset to null and the physical slot is
  removed.
- The primary changes wal_level to a level lower than logical.

The slots synchronization status on the standby can be monitored using
'synced' column of pg_replication_slots view.
---
 doc/src/sgml/bgworker.sgml                    |   65 +-
 doc/src/sgml/config.sgml                      |   27 +-
 doc/src/sgml/logicaldecoding.sgml             |   40 +
 doc/src/sgml/protocol.sgml                    |    6 +-
 doc/src/sgml/system-views.sgml                |   22 +-
 src/backend/access/transam/xlog.c             |    5 +-
 src/backend/access/transam/xlogrecovery.c     |   15 +
 src/backend/catalog/system_views.sql          |    3 +-
 src/backend/postmaster/bgworker.c             |    4 +
 src/backend/postmaster/postmaster.c           |   10 +
 .../libpqwalreceiver/libpqwalreceiver.c       |   41 +
 src/backend/replication/logical/Makefile      |    1 +
 src/backend/replication/logical/logical.c     |   12 +
 src/backend/replication/logical/meson.build   |    1 +
 src/backend/replication/logical/slotsync.c    | 1222 +++++++++++++++++
 src/backend/replication/slot.c                |   59 +-
 src/backend/replication/slotfuncs.c           |   26 +-
 src/backend/replication/walreceiverfuncs.c    |   16 +
 src/backend/replication/walsender.c           |   19 +-
 src/backend/storage/ipc/ipci.c                |    2 +
 src/backend/tcop/postgres.c                   |   11 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/misc/guc_tables.c           |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/postmaster/bgworker.h             |    1 +
 src/include/replication/logicalworker.h       |    1 +
 src/include/replication/slot.h                |   17 +-
 src/include/replication/walreceiver.h         |   11 +
 src/include/replication/walsender.h           |    3 +
 src/include/replication/worker_internal.h     |   10 +
 .../t/040_standby_failover_slots_sync.pl      |  167 ++-
 src/test/regress/expected/rules.out           |    5 +-
 src/test/regress/expected/sysviews.out        |    3 +-
 src/tools/pgindent/typedefs.list              |    2 +
 35 files changed, 1796 insertions(+), 49 deletions(-)
 create mode 100644 src/backend/replication/logical/slotsync.c

diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml
index 2c393385a9..a7cfe6c58c 100644
--- a/doc/src/sgml/bgworker.sgml
+++ b/doc/src/sgml/bgworker.sgml
@@ -114,18 +114,59 @@ typedef struct BackgroundWorker
 
   <para>
    <structfield>bgw_start_time</structfield> is the server state during which
-   <command>postgres</command> should start the process; it can be one of
-   <literal>BgWorkerStart_PostmasterStart</literal> (start as soon as
-   <command>postgres</command> itself has finished its own initialization; processes
-   requesting this are not eligible for database connections),
-   <literal>BgWorkerStart_ConsistentState</literal> (start as soon as a consistent state
-   has been reached in a hot standby, allowing processes to connect to
-   databases and run read-only queries), and
-   <literal>BgWorkerStart_RecoveryFinished</literal> (start as soon as the system has
-   entered normal read-write state).  Note the last two values are equivalent
-   in a server that's not a hot standby.  Note that this setting only indicates
-   when the processes are to be started; they do not stop when a different state
-   is reached.
+   <command>postgres</command> should start the process. Note that this setting
+   only indicates when the processes are to be started; they do not stop when
+   a different state is reached. Possible values are:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>BgWorkerStart_PostmasterStart</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_PostmasterStart</primary></indexterm>
+       Start as soon as postgres itself has finished its own initialization;
+       processes requesting this are not eligible for database connections.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState</primary></indexterm>
+       Start as soon as a consistent state has been reached in a hot-standby,
+       allowing processes to connect to databases and run read-only queries.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_ConsistentState_HotStandby</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_ConsistentState_HotStandby</primary></indexterm>
+       Same meaning as <literal>BgWorkerStart_ConsistentState</literal> but
+       it is more strict in terms of the server i.e. start the worker only
+       if it is hot-standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>BgWorkerStart_RecoveryFinished</literal></term>
+     <listitem>
+      <para>
+       <indexterm><primary>BgWorkerStart_RecoveryFinished</primary></indexterm>
+       Start as soon as the system has entered normal read-write state. Note
+       that the <literal>BgWorkerStart_ConsistentState</literal> and
+      <literal>BgWorkerStart_RecoveryFinished</literal> are equivalent
+       in a server that's not a hot standby.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
   </para>
 
   <para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5..75bb48b795 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4612,8 +4612,13 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
           <varname>primary_conninfo</varname> string, or in a separate
           <filename>~/.pgpass</filename> file on the standby server (use
           <literal>replication</literal> as the database name).
-          Do not specify a database name in the
-          <varname>primary_conninfo</varname> string.
+         </para>
+         <para>
+          If slot synchronization is enabled (see
+          <xref linkend="logicaldecoding-replication-slots-synchronization"/>)
+          then it is also necessary to specify a valid <literal>dbname</literal>
+          in the <varname>primary_conninfo</varname> string. This will only be
+          used for slot synchronization. It is ignored for streaming.
          </para>
          <para>
           This parameter can only be set in the <filename>postgresql.conf</filename>
@@ -4938,6 +4943,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable-syncslot" xreflabel="enable_syncslot">
+      <term><varname>enable_syncslot</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_syncslot</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        It enables a physical standby to synchronize logical failover slots
+        from the primary server so that logical subscribers are not blocked
+        after failover.
+       </para>
+       <para>
+        It is disabled by default. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cd152d4ced..9df5ff0145 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -358,6 +358,46 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
+
+   </sect2>
+
+   <sect2 id="logicaldecoding-replication-slots-synchronization">
+    <title>Replication Slot Synchronization</title>
+    <para>
+     A logical replication slot on the primary can be synchronized to the hot
+     standby by enabling the <literal>failover</literal> option during slot
+     creation and setting
+     <link linkend="guc-enable-syncslot"><varname>enable_syncslot</varname></link>
+     on the standby. For the synchronization
+     to work, it is mandatory to have a physical replication slot between the
+     primary and the standby, and
+     <link linkend="guc-hot-standby-feedback"><varname>hot_standby_feedback</varname></link>
+     must be enabled on the standby. It is also necessary to specify a valid
+     <literal>dbname</literal> in the
+     <link linkend="guc-primary-conninfo"><varname>primary_conninfo</varname></link>
+     string, which is used for slot synchronization and is ignored for streaming.
+    </para>
+
+    <para>
+     The ability to resume logical replication after failover depends upon the
+     <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>synced</structfield>
+     value for the synchronized slots on the standby at the time of failover.
+     Only persistent slots that have attained synced state as true on the standby
+     before failover can be used for logical replication after failover.
+     Temporary slots will be dropped, therefore logical replication for those
+     slots cannot be resumed. For example, if the synchronized slot could not
+     become persistent on the standby due to a disabled subscription, then the
+     subscription cannot be resumed after failover even when it is enabled.
+    </para>
+
+    <para>
+     To resume logical replication after failover from the synced logical
+     slots, the subscription's 'conninfo' must be altered to point to the
+     new primary server. This is done using
+     <link linkend="sql-altersubscription-params-connection"><command>ALTER SUBSCRIPTION ... CONNECTION</command></link>.
+     It is recommended that subscriptions are first disabled before promoting
+     the standby and are enabled back after altering the connection string.
+    </para>
    </sect2>
 
    <sect2 id="logicaldecoding-explanation-output-plugins">
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index bb4fef1f51..f8ef2ad2ab 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -2065,7 +2065,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
           The default is false.
          </para>
         </listitem>
@@ -2165,7 +2166,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
         <term><literal>FAILOVER [ <replaceable class="parameter">boolean</replaceable> ]</literal></term>
         <listitem>
          <para>
-          If true, the slot is enabled to be synced to the standbys.
+          If true, the slot is enabled to be synced to the standbys
+          so that logical replication can be resumed after failover.
          </para>
         </listitem>
        </varlistentry>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index dd468b31ea..4ea2177b34 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2561,10 +2561,28 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        <structfield>failover</structfield> <type>bool</type>
       </para>
       <para>
-       True if this is a logical slot enabled to be synced to the standbys.
-       Always false for physical slots.
+       True if this is a logical slot enabled to be synced to the standbys
+       so that logical replication can be resumed from the new primary
+       after failover. Always false for physical slots.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>synced</structfield> <type>bool</type>
+      </para>
+      <para>
+      True if this is a logical slot that was synced from a primary server.
+      </para>
+      <para>
+       On a hot standby, the slots with the synced column marked as true can
+       neither be used for logical decoding nor dropped by the user. The value
+       of this column has no meaning on the primary server; the column value on
+       the primary is default false for all slots but may (if leftover from a
+       promoted standby) also be true.
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 478377c4a2..2d66d0d84b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -3596,6 +3596,9 @@ XLogGetLastRemovedSegno(void)
 /*
  * Return the oldest WAL segment on the given TLI that still exists in
  * XLOGDIR, or 0 if none.
+ *
+ * If the given TLI is 0, return the oldest WAL segment among all the currently
+ * existing WAL segments.
  */
 XLogSegNo
 XLogGetOldestSegno(TimeLineID tli)
@@ -3619,7 +3622,7 @@ XLogGetOldestSegno(TimeLineID tli)
 						 wal_segment_size);
 
 		/* Ignore anything that's not from the TLI of interest. */
-		if (tli != file_tli)
+		if (tli != 0 && tli != file_tli)
 			continue;
 
 		/* If it's the oldest so far, update oldest_segno. */
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bb472da27..87b49d524a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1467,6 +1468,20 @@ FinishWalRecovery(void)
 	 */
 	XLogShutdownWalRcv();
 
+	/*
+	 * Shutdown the slot sync workers to prevent potential conflicts between
+	 * user processes and slotsync workers after a promotion.
+	 *
+	 * We do not update the 'synced' column from true to false here, as any
+	 * failed update could leave 'synced' column false for some slots. This
+	 * could cause issues during slot sync after restarting the server as a
+	 * standby. While updating after switching to the new timeline is an
+	 * option, it does not simplify the handling for 'synced' column.
+	 * Therefore, we retain the 'synced' column as true after promotion as it
+	 * may provide useful information about the slot origin.
+	 */
+	ShutDownSlotSync();
+
 	/*
 	 * We are now done reading the xlog from stream. Turn off streaming
 	 * recovery to force fetching the files (which would be required at end of
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6791bff9dd..04227a72d1 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1024,7 +1024,8 @@ CREATE VIEW pg_replication_slots AS
             L.safe_wal_size,
             L.two_phase,
             L.conflict_reason,
-            L.failover
+            L.failover,
+            L.synced
     FROM pg_get_replication_slots() AS L
             LEFT JOIN pg_database D ON (L.datoid = D.oid);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 67f92c24db..46828b8a89 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -21,6 +21,7 @@
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
 #include "storage/dsm.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -129,6 +130,9 @@ static const struct
 	{
 		"ApplyWorkerMain", ApplyWorkerMain
 	},
+	{
+		"ReplSlotSyncWorkerMain", ReplSlotSyncWorkerMain
+	},
 	{
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index feb471dd1d..36795f91f0 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/pg_shmem.h"
@@ -1010,6 +1011,12 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	ApplyLauncherRegister();
 
+	/*
+	 * Register the slot sync worker here to kick start slot-sync operation
+	 * sooner on the physical standby.
+	 */
+	SlotSyncWorkerRegister();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
@@ -5799,6 +5806,9 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
 		case PM_HOT_STANDBY:
 			if (start_time == BgWorkerStart_ConsistentState)
 				return true;
+			if (start_time == BgWorkerStart_ConsistentState_HotStandby &&
+				pmState != PM_RUN)
+				return true;
 			/* fall through */
 
 		case PM_RECOVERY:
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 2439733b55..61eeaa17b2 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -33,6 +33,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_lsn.h"
 #include "utils/tuplestore.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC;
 
@@ -57,6 +58,7 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
 static char *libpqrcv_identify_system(WalReceiverConn *conn,
 									  TimeLineID *primary_tli);
+static char *libpqrcv_get_dbname_from_conninfo(const char *conninfo);
 static int	libpqrcv_server_version(WalReceiverConn *conn);
 static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn,
 											 TimeLineID tli, char **filename,
@@ -99,6 +101,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_send = libpqrcv_send,
 	.walrcv_create_slot = libpqrcv_create_slot,
 	.walrcv_alter_slot = libpqrcv_alter_slot,
+	.walrcv_get_dbname_from_conninfo = libpqrcv_get_dbname_from_conninfo,
 	.walrcv_get_backend_pid = libpqrcv_get_backend_pid,
 	.walrcv_exec = libpqrcv_exec,
 	.walrcv_disconnect = libpqrcv_disconnect
@@ -471,6 +474,44 @@ libpqrcv_server_version(WalReceiverConn *conn)
 	return PQserverVersion(conn->streamConn);
 }
 
+/*
+ * Get database name from the primary server's conninfo.
+ *
+ * If dbname is not found in connInfo, return NULL value.
+ */
+static char *
+libpqrcv_get_dbname_from_conninfo(const char *connInfo)
+{
+	PQconninfoOption *opts;
+	char	   *dbname = NULL;
+	char	   *err = NULL;
+
+	opts = PQconninfoParse(connInfo, &err);
+	if (opts == NULL)
+	{
+		/* The error string is malloc'd, so we must free it explicitly */
+		char	   *errcopy = err ? pstrdup(err) : "out of memory";
+
+		PQfreemem(err);
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid connection string syntax: %s", errcopy)));
+	}
+
+	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+	{
+		/*
+		 * If multiple dbnames are specified, then the last one will be
+		 * returned
+		 */
+		if (strcmp(opt->keyword, "dbname") == 0 && opt->val &&
+			opt->val[0] != '\0')
+			dbname = pstrdup(opt->val);
+	}
+
+	return dbname;
+}
+
 /*
  * Start streaming WAL data from given streaming options.
  *
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index 2dc25e37bb..ba03eeff1c 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	slotsync.o \
 	snapbuild.o \
 	tablesync.o \
 	worker.o
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index ca09c683f1..5aefb10ecb 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -524,6 +524,18 @@ CreateDecodingContext(XLogRecPtr start_lsn,
 				 errmsg("replication slot \"%s\" was not created in this database",
 						NameStr(slot->data.name))));
 
+	/*
+	 * Do not allow consumption of a "synchronized" slot until the standby
+	 * gets promoted.
+	 */
+	if (RecoveryInProgress() && slot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot use replication slot \"%s\" for logical"
+					   " decoding", NameStr(slot->data.name)),
+				errdetail("This slot is being synced from the primary server."),
+				errhint("Specify another replication slot."));
+
 	/*
 	 * Check if slot has been invalidated due to max_slot_wal_keep_size. Avoid
 	 * "cannot get changes" wording in this errmsg because that'd be
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 1050eb2c09..3dec36a6de 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'slotsync.c',
   'snapbuild.c',
   'tablesync.c',
   'worker.c',
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
new file mode 100644
index 0000000000..e44474dd58
--- /dev/null
+++ b/src/backend/replication/logical/slotsync.c
@@ -0,0 +1,1222 @@
+/*-------------------------------------------------------------------------
+ * slotsync.c
+ *	   PostgreSQL worker for synchronizing slots to a standby server from the
+ *         primary server.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/slotsync.c
+ *
+ * This file contains the code for slot sync worker on a physical standby
+ * to fetch logical failover slots information from the primary server,
+ * create the slots on the standby and synchronize them periodically.
+ *
+ * While creating the slot on physical standby, if the local restart_lsn and/or
+ * local catalog_xmin is ahead of those on the remote then the worker cannot
+ * create the local slot in sync with the primary server because that would
+ * mean moving the local slot backwards and the standby might not have WALs
+ * retained for old LSN. In this case, the worker will mark the slot as
+ * RS_TEMPORARY. Once the primary server catches up, the worker will mark the
+ * slot as RS_PERSISTENT (which means sync-ready) and will perform the sync
+ * periodically.
+ *
+ * The worker also takes care of dropping the slots which were created by it
+ * and are currently not needed to be synchronized.
+ *
+ * It waits for a period of time before the next synchronization, with the
+ * duration varying based on whether any slots were updated during the last
+ * cycle. Refer to the comments above wait_for_slot_activity() for more details.
+ *
+ * Slot synchronization is currently not supported on the cascading standby.
+ *---------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogrecovery.h"
+#include "catalog/pg_database.h"
+#include "commands/dbcommands.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "replication/logical.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/walreceiver.h"
+#include "replication/worker_internal.h"
+#include "storage/ipc.h"
+#include "storage/lmgr.h"
+#include "storage/procarray.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc_hooks.h"
+#include "utils/pg_lsn.h"
+#include "utils/varlena.h"
+
+/*
+ * Structure to hold information fetched from the primary server about a logical
+ * replication slot.
+ */
+typedef struct RemoteSlot
+{
+	char	   *name;
+	char	   *plugin;
+	char	   *database;
+	bool		two_phase;
+	bool		failover;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	confirmed_lsn;
+	TransactionId catalog_xmin;
+
+	/* RS_INVAL_NONE if valid, or the reason of invalidation */
+	ReplicationSlotInvalidationCause invalidated;
+} RemoteSlot;
+
+/*
+ * Struct for sharing information between startup process and slot
+ * sync worker.
+ *
+ * Slot sync worker's pid is needed by the startup process in order to
+ * shut it down during promotion. Startup process shuts down the slot
+ * sync worker and also sets stopSignaled=true to handle the race condition
+ * when postmaster has not noticed the promotion yet and thus may end up
+ * restarting slot sync worker. If stopSignaled is set, the worker will
+ * exit in such a case.
+ */
+typedef struct SlotSyncWorkerCtxStruct
+{
+	pid_t		pid;
+	bool		stopSignaled;
+	slock_t		mutex;
+} SlotSyncWorkerCtxStruct;
+
+SlotSyncWorkerCtxStruct *SlotSyncWorker = NULL;
+
+/* GUC variable */
+bool		enable_syncslot = false;
+
+/*
+ * The sleep time (ms) between slot-sync cycles varies dynamically
+ * (within a MIN/MAX range) according to slot activity. See
+ * wait_for_slot_activity() for details.
+ */
+#define MIN_WORKER_NAPTIME_MS  200
+#define MAX_WORKER_NAPTIME_MS  30000	/* 30s */
+static long sleep_ms = MIN_WORKER_NAPTIME_MS;
+
+static void ProcessSlotSyncInterrupts(WalReceiverConn *wrconn);
+
+/*
+ * If necessary, update local slot metadata based on the data from the remote
+ * slot.
+ *
+ * If no update was needed (the data of the remote slot is the same as the
+ * local slot) return false, otherwise true.
+ */
+static bool
+local_slot_update(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+	NameData	plugin_name;
+
+	Assert(slot->data.invalidated == RS_INVAL_NONE);
+
+	if (strcmp(remote_slot->plugin, NameStr(slot->data.plugin)) == 0 &&
+		remote_dbid == slot->data.database &&
+		remote_slot->restart_lsn == slot->data.restart_lsn &&
+		remote_slot->catalog_xmin == slot->data.catalog_xmin &&
+		remote_slot->two_phase == slot->data.two_phase &&
+		remote_slot->failover == slot->data.failover &&
+		remote_slot->confirmed_lsn == slot->data.confirmed_flush)
+		return false;
+
+	/* Avoid expensive operations while holding a spinlock. */
+	namestrcpy(&plugin_name, remote_slot->plugin);
+
+	SpinLockAcquire(&slot->mutex);
+	slot->data.plugin = plugin_name;
+	slot->data.database = remote_dbid;
+	slot->data.two_phase = remote_slot->two_phase;
+	slot->data.failover = remote_slot->failover;
+	slot->data.restart_lsn = remote_slot->restart_lsn;
+	slot->data.confirmed_flush = remote_slot->confirmed_lsn;
+	slot->data.catalog_xmin = remote_slot->catalog_xmin;
+	slot->effective_catalog_xmin = remote_slot->catalog_xmin;
+	SpinLockRelease(&slot->mutex);
+
+	if (remote_slot->catalog_xmin != slot->data.catalog_xmin)
+		ReplicationSlotsComputeRequiredXmin(false);
+
+	if (remote_slot->restart_lsn != slot->data.restart_lsn)
+		ReplicationSlotsComputeRequiredLSN();
+
+	return true;
+}
+
+/*
+ * Get list of local logical slots which are synchronized from
+ * the primary server.
+ */
+static List *
+get_local_synced_slots(void)
+{
+	List	   *local_slots = NIL;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	for (int i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		/* Check if it is a synchronized slot */
+		if (s->in_use && s->data.synced)
+		{
+			Assert(SlotIsLogical(s));
+			local_slots = lappend(local_slots, s);
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return local_slots;
+}
+
+/*
+ * Helper function to check if local_slot is present in remote_slots list.
+ *
+ * It also checks if the slot on the standby server was invalidated while the
+ * corresponding remote slot in the list remained valid. If found so, it sets
+ * the locally_invalidated flag to true.
+ */
+static bool
+check_sync_slot_on_remote(ReplicationSlot *local_slot, List *remote_slots,
+						  bool *locally_invalidated)
+{
+	foreach_ptr(RemoteSlot, remote_slot, remote_slots)
+	{
+		if (strcmp(remote_slot->name, NameStr(local_slot->data.name)) == 0)
+		{
+			/*
+			 * If remote slot is not invalidated but local slot is marked as
+			 * invalidated, then set the bool.
+			 */
+			SpinLockAcquire(&local_slot->mutex);
+			*locally_invalidated =
+				(remote_slot->invalidated == RS_INVAL_NONE) &&
+				(local_slot->data.invalidated != RS_INVAL_NONE);
+			SpinLockRelease(&local_slot->mutex);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Drop obsolete slots
+ *
+ * Drop the slots that no longer need to be synced i.e. these either do not
+ * exist on the primary or are no longer enabled for failover.
+ *
+ * Additionally, it drops slots that are valid on the primary but got
+ * invalidated on the standby. This situation may occur due to the following
+ * reasons:
+ * - The max_slot_wal_keep_size on the standby is insufficient to retain WAL
+ *   records from the restart_lsn of the slot.
+ * - primary_slot_name is temporarily reset to null and the physical slot is
+ *   removed.
+ * - The primary changes wal_level to a level lower than logical.
+ *
+ * The assumption is that these dropped slots will get recreated in next
+ * sync-cycle and it is okay to drop and recreate such slots as long as these
+ * are not consumable on the standby (which is the case currently).
+ */
+static void
+drop_obsolete_slots(List *remote_slot_list)
+{
+	List	   *local_slots = get_local_synced_slots();
+
+	foreach_ptr(ReplicationSlot, local_slot, local_slots)
+	{
+		bool		remote_exists = false;
+		bool		locally_invalidated = false;
+
+		remote_exists = check_sync_slot_on_remote(local_slot, remote_slot_list,
+												  &locally_invalidated);
+
+		/*
+		 * Drop the local slot either if it is not in the remote slots list or
+		 * is invalidated while remote slot is still valid.
+		 */
+		if (!remote_exists || locally_invalidated)
+		{
+			/*
+			 * Use shared lock to prevent a conflict with
+			 * ReplicationSlotsDropDBSlots(), trying to drop the same slot
+			 * while drop-database operation.
+			 */
+			LockSharedObject(DatabaseRelationId, local_slot->data.database,
+							 0, AccessShareLock);
+
+			ReplicationSlotAcquire(NameStr(local_slot->data.name), true);
+			Assert(MyReplicationSlot->data.synced);
+			ReplicationSlotDropAcquired();
+
+			UnlockSharedObject(DatabaseRelationId, local_slot->data.database,
+							   0, AccessShareLock);
+
+			ereport(LOG,
+					errmsg("dropped replication slot \"%s\" of dbid %d",
+						   NameStr(local_slot->data.name),
+						   local_slot->data.database));
+		}
+	}
+}
+
+/*
+ * Reserve WAL for the currently active slot using the specified WAL location
+ * (restart_lsn).
+ *
+ * If the given WAL location has been removed, reserve WAL using the oldest
+ * existing WAL segment.
+ */
+static void
+reserve_wal_for_slot(XLogRecPtr restart_lsn)
+{
+	XLogSegNo	oldest_segno;
+	XLogSegNo	segno;
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	Assert(slot != NULL);
+	Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn));
+
+	while (true)
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+
+		/* Prevent WAL removal as fast as possible */
+		ReplicationSlotsComputeRequiredLSN();
+
+		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
+
+		/*
+		 * Find the oldest existing WAL segment file.
+		 *
+		 * Normally, we can determine it by using the last removed segment
+		 * number. However, if no WAL segment files have been removed by a
+		 * checkpoint since startup, we need to search for the oldest segment
+		 * file currently existing in XLOGDIR.
+		 */
+		oldest_segno = XLogGetLastRemovedSegno() + 1;
+
+		if (oldest_segno == 1)
+			oldest_segno = XLogGetOldestSegno(0);
+
+		/*
+		 * If all required WAL is still there, great, otherwise retry. The
+		 * slot should prevent further removal of WAL, unless there's a
+		 * concurrent ReplicationSlotsComputeRequiredLSN() after we've written
+		 * the new restart_lsn above, so normally we should never need to loop
+		 * more than twice.
+		 */
+		if (segno >= oldest_segno)
+			break;
+
+		/* Retry using the location of the oldest wal segment */
+		XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn);
+	}
+}
+
+/*
+ * Update the LSNs and persist the slot for further syncs if the remote
+ * restart_lsn and catalog_xmin have caught up with the local ones, otherwise
+ * do nothing.
+ *
+ * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise
+ * false.
+ */
+static bool
+update_and_persist_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot = MyReplicationSlot;
+
+	/*
+	 * Check if the primary server has caught up. Refer to the comment atop
+	 * the file for details on this check.
+	 *
+	 * We also need to check if remote_slot's confirmed_lsn becomes valid. It
+	 * is possible to get null values for confirmed_lsn and catalog_xmin if on
+	 * the primary server the slot is just created with a valid restart_lsn
+	 * and slot-sync worker has fetched the slot before the primary server
+	 * could set valid confirmed_lsn and catalog_xmin.
+	 */
+	if (remote_slot->restart_lsn < slot->data.restart_lsn ||
+		XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) ||
+		TransactionIdPrecedes(remote_slot->catalog_xmin,
+							  slot->data.catalog_xmin))
+	{
+		/*
+		 * The remote slot didn't catch up to locally reserved position.
+		 *
+		 * We do not drop the slot because the restart_lsn can be ahead of the
+		 * current location when recreating the slot in the next cycle. It may
+		 * take more time to create such a slot. Therefore, we keep this slot
+		 * and attempt the wait and synchronization in the next cycle.
+		 */
+		return false;
+	}
+
+	/* First time slot update, the function must return true */
+	if (!local_slot_update(remote_slot, remote_dbid))
+		elog(ERROR, "failed to update slot");
+
+	ReplicationSlotPersist();
+
+	ereport(LOG,
+			errmsg("newly created slot \"%s\" is sync-ready now",
+				   remote_slot->name));
+
+	return true;
+}
+
+/*
+ * Synchronize single slot to given position.
+ *
+ * This creates a new slot if there is no existing one and updates the
+ * metadata of the slot as per the data received from the primary server.
+ *
+ * The slot is created as a temporary slot and stays in the same state until the
+ * the remote_slot catches up with locally reserved position and local slot is
+ * updated. The slot is then persisted and is considered as sync-ready for
+ * periodic syncs.
+ *
+ * Returns TRUE if the local slot is updated.
+ */
+static bool
+synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+{
+	ReplicationSlot *slot;
+	bool		slot_updated = false;
+	XLogRecPtr	latestFlushPtr;
+
+	/*
+	 * Make sure that concerned WAL is received and flushed before syncing
+	 * slot to target lsn received from the primary server.
+	 *
+	 * This check will never pass if on the primary server, user has
+	 * configured standby_slot_names GUC correctly, otherwise this can hit
+	 * frequently.
+	 */
+	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
+	if (remote_slot->confirmed_lsn > latestFlushPtr)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization as the received slot sync"
+					   " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X",
+					   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn),
+					   remote_slot->name,
+					   LSN_FORMAT_ARGS(latestFlushPtr)));
+
+		return false;
+	}
+
+	/* Search for the named slot */
+	if ((slot = SearchNamedReplicationSlot(remote_slot->name, true)))
+	{
+		bool		synced;
+
+		SpinLockAcquire(&slot->mutex);
+		synced = slot->data.synced;
+		SpinLockRelease(&slot->mutex);
+
+		/* User created slot with the same name exists, raise ERROR. */
+		if (!synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("exiting from slot synchronization because same"
+						   " name slot \"%s\" already exists on the standby",
+						   remote_slot->name));
+
+		/*
+		 * Slot created by the slot sync worker exists, sync it.
+		 *
+		 * It is important to acquire the slot here before checking
+		 * invalidation. If we don't acquire the slot first, there could be a
+		 * race condition that the local slot could be invalidated just after
+		 * checking the 'invalidated' flag here and we could end up
+		 * overwriting 'invalidated' flag to remote_slot's value. See
+		 * InvalidatePossiblyObsoleteSlot() where it invalidates slot directly
+		 * if the slot is not acquired by other processes.
+		 */
+		ReplicationSlotAcquire(remote_slot->name, true);
+
+		Assert(slot == MyReplicationSlot);
+
+		/*
+		 * Copy the invalidation cause from remote only if local slot is not
+		 * invalidated locally, we don't want to overwrite existing one.
+		 */
+		if (slot->data.invalidated == RS_INVAL_NONE)
+		{
+			SpinLockAcquire(&slot->mutex);
+			slot->data.invalidated = remote_slot->invalidated;
+			SpinLockRelease(&slot->mutex);
+
+			/* Make sure the invalidated state persists across server restart */
+			ReplicationSlotMarkDirty();
+			ReplicationSlotSave();
+			slot_updated = true;
+		}
+
+		/* Skip the sync of an invalidated slot */
+		if (slot->data.invalidated != RS_INVAL_NONE)
+		{
+			ReplicationSlotRelease();
+			return slot_updated;
+		}
+
+		/* Slot not ready yet, let's attempt to make it sync-ready now. */
+		if (slot->data.persistency == RS_TEMPORARY)
+		{
+			slot_updated = update_and_persist_slot(remote_slot, remote_dbid);
+		}
+
+		/* Slot ready for sync, so sync it. */
+		else
+		{
+			/*
+			 * Sanity check: As long as the invalidations are handled
+			 * appropriately as above, this should never happen.
+			 */
+			if (remote_slot->restart_lsn < slot->data.restart_lsn)
+				elog(ERROR,
+					 "cannot synchronize local slot \"%s\" LSN(%X/%X)"
+					 " to remote slot's LSN(%X/%X) as synchronization"
+					 " would move it backwards", remote_slot->name,
+					 LSN_FORMAT_ARGS(slot->data.restart_lsn),
+					 LSN_FORMAT_ARGS(remote_slot->restart_lsn));
+
+			/* Make sure the slot changes persist across server restart */
+			if (local_slot_update(remote_slot, remote_dbid))
+			{
+				slot_updated = true;
+				ReplicationSlotMarkDirty();
+				ReplicationSlotSave();
+			}
+		}
+	}
+	/* Otherwise create the slot first. */
+	else
+	{
+		NameData	plugin_name;
+		TransactionId xmin_horizon = InvalidTransactionId;
+
+		/* Skip creating the local slot if remote_slot is invalidated already */
+		if (remote_slot->invalidated != RS_INVAL_NONE)
+			return false;
+
+		ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY,
+							  remote_slot->two_phase,
+							  remote_slot->failover,
+							  true);
+
+		/* For shorter lines. */
+		slot = MyReplicationSlot;
+
+		/* Avoid expensive operations while holding a spinlock. */
+		namestrcpy(&plugin_name, remote_slot->plugin);
+
+		SpinLockAcquire(&slot->mutex);
+		slot->data.database = remote_dbid;
+		slot->data.plugin = plugin_name;
+		SpinLockRelease(&slot->mutex);
+
+		reserve_wal_for_slot(remote_slot->restart_lsn);
+
+		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+		xmin_horizon = GetOldestSafeDecodingTransactionId(true);
+		SpinLockAcquire(&slot->mutex);
+		slot->effective_catalog_xmin = xmin_horizon;
+		slot->data.catalog_xmin = xmin_horizon;
+		SpinLockRelease(&slot->mutex);
+		ReplicationSlotsComputeRequiredXmin(true);
+		LWLockRelease(ProcArrayLock);
+
+		(void) update_and_persist_slot(remote_slot, remote_dbid);
+		slot_updated = true;
+	}
+
+	ReplicationSlotRelease();
+
+	return slot_updated;
+}
+
+/*
+ * Maps the pg_replication_slots.conflict_reason text value to
+ * ReplicationSlotInvalidationCause enum value
+ */
+static ReplicationSlotInvalidationCause
+get_slot_invalidation_cause(char *conflict_reason)
+{
+	Assert(conflict_reason);
+
+	if (strcmp(conflict_reason, SLOT_INVAL_WAL_REMOVED_TEXT) == 0)
+		return RS_INVAL_WAL_REMOVED;
+	else if (strcmp(conflict_reason, SLOT_INVAL_HORIZON_TEXT) == 0)
+		return RS_INVAL_HORIZON;
+	else if (strcmp(conflict_reason, SLOT_INVAL_WAL_LEVEL_TEXT) == 0)
+		return RS_INVAL_WAL_LEVEL;
+	else
+		Assert(0);
+
+	/* Keep compiler quiet */
+	return RS_INVAL_NONE;
+}
+
+/*
+ * Synchronize slots.
+ *
+ * Gets the failover logical slots info from the primary server and updates
+ * the slots locally. Creates the slots if not present on the standby.
+ *
+ * Returns TRUE if any of the slots gets updated in this sync-cycle.
+ */
+static bool
+synchronize_slots(WalReceiverConn *wrconn)
+{
+#define SLOTSYNC_COLUMN_COUNT 9
+	Oid			slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID,
+	LSNOID, XIDOID, BOOLOID, BOOLOID, TEXTOID, TEXTOID};
+
+	WalRcvExecResult *res;
+	TupleTableSlot *tupslot;
+	StringInfoData s;
+	List	   *remote_slot_list = NIL;
+	bool		some_slot_updated = false;
+	XLogRecPtr	latestWalEnd;
+
+	/*
+	 * The primary_slot_name is not set yet or WALs not received yet.
+	 * Synchronization is not possible if the walreceiver is not started.
+	 */
+	latestWalEnd = GetWalRcvLatestWalEnd();
+	SpinLockAcquire(&WalRcv->mutex);
+	if ((WalRcv->slotname[0] == '\0') ||
+		XLogRecPtrIsInvalid(latestWalEnd))
+	{
+		SpinLockRelease(&WalRcv->mutex);
+		return false;
+	}
+	SpinLockRelease(&WalRcv->mutex);
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	initStringInfo(&s);
+
+	/* Construct query to fetch slots with failover enabled. */
+	appendStringInfo(&s,
+					 "SELECT slot_name, plugin, confirmed_flush_lsn,"
+					 " restart_lsn, catalog_xmin, two_phase, failover,"
+					 " database, conflict_reason"
+					 " FROM pg_catalog.pg_replication_slots"
+					 " WHERE failover and NOT temporary");
+
+	/* Execute the query */
+	res = walrcv_exec(wrconn, s.data, SLOTSYNC_COLUMN_COUNT, slotRow);
+	pfree(s.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch failover logical slots info from the primary server: %s",
+					   res->err));
+
+	/* Construct the remote_slot tuple and synchronize each slot locally */
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+	{
+		bool		isnull;
+		RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot));
+		Datum		d;
+		int			col = 0;
+
+		remote_slot->name = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															 &isnull));
+		Assert(!isnull);
+
+		remote_slot->plugin = TextDatumGetCString(slot_getattr(tupslot, ++col,
+															   &isnull));
+		Assert(!isnull);
+
+		/*
+		 * It is possible to get null values for LSN and Xmin if slot is
+		 * invalidated on the primary server, so handle accordingly.
+		 */
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->confirmed_lsn = isnull ? InvalidXLogRecPtr :
+			DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->restart_lsn = isnull ? InvalidXLogRecPtr : DatumGetLSN(d);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->catalog_xmin = isnull ? InvalidTransactionId :
+			DatumGetTransactionId(d);
+
+		remote_slot->two_phase = DatumGetBool(slot_getattr(tupslot, ++col,
+														   &isnull));
+		Assert(!isnull);
+
+		remote_slot->failover = DatumGetBool(slot_getattr(tupslot, ++col,
+														  &isnull));
+		Assert(!isnull);
+
+		remote_slot->database = TextDatumGetCString(slot_getattr(tupslot,
+																 ++col, &isnull));
+		Assert(!isnull);
+
+		d = slot_getattr(tupslot, ++col, &isnull);
+		remote_slot->invalidated = isnull ? RS_INVAL_NONE :
+			get_slot_invalidation_cause(TextDatumGetCString(d));
+
+		/* Sanity check */
+		Assert(col == SLOTSYNC_COLUMN_COUNT);
+
+		/* Create list of remote slots */
+		remote_slot_list = lappend(remote_slot_list, remote_slot);
+
+		ExecClearTuple(tupslot);
+	}
+
+	/* Drop local slots that no longer need to be synced. */
+	drop_obsolete_slots(remote_slot_list);
+
+	/* Now sync the slots locally */
+	foreach_ptr(RemoteSlot, remote_slot, remote_slot_list)
+	{
+		Oid			remote_dbid = get_database_oid(remote_slot->database, false);
+
+		/*
+		 * Use shared lock to prevent a conflict with
+		 * ReplicationSlotsDropDBSlots(), trying to drop the same slot while
+		 * drop-database operation.
+		 */
+		LockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+
+		some_slot_updated |= synchronize_one_slot(remote_slot, remote_dbid);
+
+		UnlockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock);
+	}
+
+	/* We are done, free remote_slot_list elements */
+	list_free_deep(remote_slot_list);
+
+	walrcv_clear_result(res);
+
+	CommitTransactionCommand();
+
+	return some_slot_updated;
+}
+
+/*
+ * Checks the primary server info.
+ *
+ * Using the specified primary server connection, check whether we are a
+ * cascading standby. It also validates primary_slot_name for non-cascading
+ * standbys.
+ */
+static void
+check_primary_info(WalReceiverConn *wrconn, bool *am_cascading_standby)
+{
+#define PRIMARY_INFO_OUTPUT_COL_COUNT 2
+	WalRcvExecResult *res;
+	Oid			slotRow[PRIMARY_INFO_OUTPUT_COL_COUNT] = {BOOLOID, BOOLOID};
+	StringInfoData cmd;
+	bool		isnull;
+	TupleTableSlot *tupslot;
+	bool		valid;
+	bool		remote_in_recovery;
+
+	/* The syscache access in walrcv_exec() needs a transaction env. */
+	StartTransactionCommand();
+
+	Assert(am_cascading_standby != NULL);
+
+	*am_cascading_standby = false;	/* overwritten later if cascading */
+
+	initStringInfo(&cmd);
+	appendStringInfo(&cmd,
+					 "SELECT pg_is_in_recovery(), count(*) = 1"
+					 " FROM pg_replication_slots"
+					 " WHERE slot_type='physical' AND slot_name=%s",
+					 quote_literal_cstr(PrimarySlotName));
+
+	res = walrcv_exec(wrconn, cmd.data, PRIMARY_INFO_OUTPUT_COL_COUNT, slotRow);
+	pfree(cmd.data);
+
+	if (res->status != WALRCV_OK_TUPLES)
+		ereport(ERROR,
+				errmsg("could not fetch primary_slot_name \"%s\" info from the primary server: %s",
+					   PrimarySlotName, res->err),
+				errhint("Check if \"primary_slot_name\" is configured correctly."));
+
+	tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+	if (!tuplestore_gettupleslot(res->tuplestore, true, false, tupslot))
+		elog(ERROR,
+			 "failed to fetch tuple for the primary server slot specified by \"primary_slot_name\"");
+
+	remote_in_recovery = DatumGetBool(slot_getattr(tupslot, 1, &isnull));
+	Assert(!isnull);
+
+	if (remote_in_recovery)
+	{
+		/* No need to check further, just set am_cascading_standby to true */
+		*am_cascading_standby = true;
+	}
+	else
+	{
+		/* We are a normal standby */
+		valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+		Assert(!isnull);
+
+		if (!valid)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("exiting from slot synchronization due to bad configuration"),
+			/* translator: second %s is a GUC variable name */
+					errdetail("The primary server slot \"%s\" specified by"
+							  " \"%s\" is not valid.",
+							  PrimarySlotName, "primary_slot_name"));
+	}
+
+	ExecClearTuple(tupslot);
+	walrcv_clear_result(res);
+	CommitTransactionCommand();
+}
+
+/*
+ * Check that all necessary GUCs for slot synchronization are set
+ * appropriately. If not, raise an ERROR.
+ *
+ * If all checks pass, extracts the dbname from the primary_conninfo GUC and
+ * returns it.
+ */
+static char *
+validate_parameters_and_get_dbname(void)
+{
+	char	   *dbname;
+
+	/* Sanity check. */
+	Assert(enable_syncslot);
+
+	/*
+	 * A physical replication slot(primary_slot_name) is required on the
+	 * primary to ensure that the rows needed by the standby are not removed
+	 * after restarting, so that the synchronized slot on the standby will not
+	 * be invalidated.
+	 */
+	if (PrimarySlotName == NULL || strcmp(PrimarySlotName, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_slot_name"));
+
+	/*
+	 * hot_standby_feedback must be enabled to cooperate with the physical
+	 * replication slot, which allows informing the primary about the xmin and
+	 * catalog_xmin values on the standby.
+	 */
+	if (!hot_standby_feedback)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be enabled.", "hot_standby_feedback"));
+
+	/*
+	 * Logical decoding requires wal_level >= logical and we currently only
+	 * synchronize logical slots.
+	 */
+	if (wal_level < WAL_LEVEL_LOGICAL)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"wal_level\" must be >= logical."));
+
+	/*
+	 * The primary_conninfo is required to make connection to primary for
+	 * getting slots information.
+	 */
+	if (PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0)
+		ereport(ERROR,
+		/* translator: %s is a GUC variable name */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("\"%s\" must be defined.", "primary_conninfo"));
+
+	/*
+	 * The slot sync worker needs a database connection for walrcv_exec to
+	 * work.
+	 */
+	dbname = walrcv_get_dbname_from_conninfo(PrimaryConnInfo);
+	if (dbname == NULL)
+		ereport(ERROR,
+
+		/*
+		 * translator: 'dbname' is a specific option; %s is a GUC variable
+		 * name
+		 */
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("exiting from slot synchronization due to bad configuration"),
+				errhint("'dbname' must be specified in \"%s\".", "primary_conninfo"));
+
+	return dbname;
+}
+
+/*
+ * Re-read the config file.
+ *
+ * If any of the slot sync GUCs have changed, exit the worker and
+ * let it get restarted by the postmaster.
+ */
+static void
+slotsync_reread_config(void)
+{
+	char	   *old_primary_conninfo = pstrdup(PrimaryConnInfo);
+	char	   *old_primary_slotname = pstrdup(PrimarySlotName);
+	bool		old_hot_standby_feedback = hot_standby_feedback;
+	bool		conninfo_changed;
+	bool		primary_slotname_changed;
+
+	ConfigReloadPending = false;
+	ProcessConfigFile(PGC_SIGHUP);
+
+	conninfo_changed = strcmp(old_primary_conninfo, PrimaryConnInfo) != 0;
+	primary_slotname_changed = strcmp(old_primary_slotname, PrimarySlotName) != 0;
+
+	if (conninfo_changed ||
+		primary_slotname_changed ||
+		(old_hot_standby_feedback != hot_standby_feedback))
+	{
+		ereport(LOG,
+				errmsg("slot sync worker will restart because of a parameter change"));
+
+		/* The exit code 1 will make postmaster restart this worker */
+		proc_exit(1);
+	}
+
+	pfree(old_primary_conninfo);
+	pfree(old_primary_slotname);
+}
+
+/*
+ * Interrupt handler for main loop of slot sync worker.
+ */
+static void
+ProcessSlotSyncInterrupts(WalReceiverConn *wrconn)
+{
+	CHECK_FOR_INTERRUPTS();
+
+	if (ShutdownRequestPending)
+	{
+		walrcv_disconnect(wrconn);
+		ereport(LOG,
+				errmsg("replication slot sync worker is shutting down on receiving SIGINT"));
+		proc_exit(0);
+	}
+
+	if (ConfigReloadPending)
+		slotsync_reread_config();
+}
+
+/*
+ * Cleanup function for slotsync worker.
+ *
+ * Called on slotsync worker exit.
+ */
+static void
+slotsync_worker_onexit(int code, Datum arg)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+	SlotSyncWorker->pid = InvalidPid;
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Sleep for long enough that we believe it's likely that the slots on primary
+ * get updated.
+ *
+ * If there is no slot activity the wait time between sync-cycles will double
+ * (to a maximum of 30s). If there is some slot activity the wait time between
+ * sync-cycles is reset to the minimum (200ms).
+ */
+static void
+wait_for_slot_activity(bool some_slot_updated, bool am_cascading_standby)
+{
+	int			rc;
+
+	if (am_cascading_standby)
+	{
+		/*
+		 * Slot synchronization is currently not supported on cascading
+		 * standby. So if we are on the cascading standby, we will skip the
+		 * sync and take a longer nap before we check again whether we are
+		 * still cascading standby or not.
+		 */
+		sleep_ms = MAX_WORKER_NAPTIME_MS;
+	}
+	else if (!some_slot_updated)
+	{
+		/*
+		 * No slots were updated, so double the sleep time, but not beyond the
+		 * maximum allowable value.
+		 */
+		sleep_ms = Min(sleep_ms * 2, MAX_WORKER_NAPTIME_MS);
+	}
+	else
+	{
+		/*
+		 * Some slots were updated since the last sleep, so reset the sleep
+		 * time.
+		 */
+		sleep_ms = MIN_WORKER_NAPTIME_MS;
+	}
+
+	rc = WaitLatch(MyLatch,
+				   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+				   sleep_ms,
+				   WAIT_EVENT_REPL_SLOTSYNC_MAIN);
+
+	if (rc & WL_LATCH_SET)
+		ResetLatch(MyLatch);
+}
+
+/*
+ * The main loop of our worker process.
+ *
+ * It connects to the primary server, fetches logical failover slots
+ * information periodically in order to create and sync the slots.
+ */
+void
+ReplSlotSyncWorkerMain(Datum main_arg)
+{
+	WalReceiverConn *wrconn = NULL;
+	char	   *dbname;
+	bool		am_cascading_standby;
+	char	   *err;
+
+	ereport(LOG, errmsg("replication slot sync worker started"));
+
+	on_shmem_exit(slotsync_worker_onexit, (Datum) 0);
+
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	Assert(SlotSyncWorker->pid == InvalidPid);
+
+	/*
+	 * Startup process signaled the slot sync worker to stop, so if meanwhile
+	 * postmaster ended up starting the worker again, exit.
+	 */
+	if (SlotSyncWorker->stopSignaled)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		proc_exit(0);
+	}
+
+	/* Advertise our PID so that the startup process can kill us on promotion */
+	SlotSyncWorker->pid = MyProcPid;
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	/* Setup signal handling */
+	pqsignal(SIGHUP, SignalHandlerForConfigReload);
+	pqsignal(SIGINT, SignalHandlerForShutdownRequest);
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	/* Load the libpq-specific functions */
+	load_file("libpqwalreceiver", false);
+
+	dbname = validate_parameters_and_get_dbname();
+
+	/*
+	 * Connect to the database specified by user in primary_conninfo. We need
+	 * a database connection for walrcv_exec to work. Please see comments atop
+	 * libpqrcv_exec.
+	 */
+	BackgroundWorkerInitializeConnection(dbname, NULL, 0);
+
+	/*
+	 * Establish the connection to the primary server for slots
+	 * synchronization.
+	 */
+	wrconn = walrcv_connect(PrimaryConnInfo, true, false,
+							cluster_name[0] ? cluster_name : "slotsyncworker",
+							&err);
+	if (!wrconn)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("could not connect to the primary server: %s", err));
+
+	/*
+	 * Using the specified primary server connection, check whether we are
+	 * cascading standby and validates primary_slot_name for
+	 * non-cascading-standbys.
+	 */
+	check_primary_info(wrconn, &am_cascading_standby);
+
+	/* Main wait loop */
+	for (;;)
+	{
+		bool		some_slot_updated = false;
+
+		ProcessSlotSyncInterrupts(wrconn);
+
+		if (!am_cascading_standby)
+			some_slot_updated = synchronize_slots(wrconn);
+
+		wait_for_slot_activity(some_slot_updated, am_cascading_standby);
+
+		/*
+		 * If the standby was promoted then what was previously a cascading
+		 * standby might no longer be one, so recheck each time.
+		 */
+		if (am_cascading_standby)
+			check_primary_info(wrconn, &am_cascading_standby);
+	}
+
+	/*
+	 * The slot sync worker can not get here because it will only stop when it
+	 * receives a SIGINT from the startup process, or when there is an error.
+	 */
+	Assert(false);
+}
+
+/*
+ * Is current process the slot sync worker?
+ */
+bool
+IsLogicalSlotSyncWorker(void)
+{
+	return SlotSyncWorker->pid == MyProcPid;
+}
+
+/*
+ * Shut down the slot sync worker.
+ */
+void
+ShutDownSlotSync(void)
+{
+	SpinLockAcquire(&SlotSyncWorker->mutex);
+
+	SlotSyncWorker->stopSignaled = true;
+
+	if (SlotSyncWorker->pid == InvalidPid)
+	{
+		SpinLockRelease(&SlotSyncWorker->mutex);
+		return;
+	}
+	SpinLockRelease(&SlotSyncWorker->mutex);
+
+	kill(SlotSyncWorker->pid, SIGINT);
+
+	/* Wait for it to die */
+	for (;;)
+	{
+		int			rc;
+
+		/* Wait a bit, we don't expect to have to wait long */
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					   10L, WAIT_EVENT_BGWORKER_SHUTDOWN);
+
+		if (rc & WL_LATCH_SET)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		SpinLockAcquire(&SlotSyncWorker->mutex);
+
+		/* Is it gone? */
+		if (SlotSyncWorker->pid == InvalidPid)
+			break;
+
+		SpinLockRelease(&SlotSyncWorker->mutex);
+	}
+
+	SpinLockRelease(&SlotSyncWorker->mutex);
+}
+
+/*
+ * Allocate and initialize slot sync worker shared memory
+ */
+void
+SlotSyncWorkerShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = sizeof(SlotSyncWorkerCtxStruct);
+	size = MAXALIGN(size);
+
+	SlotSyncWorker = (SlotSyncWorkerCtxStruct *)
+		ShmemInitStruct("Slot Sync Worker Data", size, &found);
+
+	if (!found)
+	{
+		memset(SlotSyncWorker, 0, size);
+		SlotSyncWorker->pid = InvalidPid;
+		SpinLockInit(&SlotSyncWorker->mutex);
+	}
+}
+
+/*
+ * Register the background worker for slots synchronization provided
+ * enable_syncslot is ON.
+ */
+void
+SlotSyncWorkerRegister(void)
+{
+	BackgroundWorker bgw;
+
+	if (!enable_syncslot)
+	{
+		ereport(LOG,
+				errmsg("skipping slot synchronization"),
+				errdetail("\"enable_syncslot\" is disabled."));
+		return;
+	}
+
+	memset(&bgw, 0, sizeof(bgw));
+
+	/* We need database connection which needs shared-memory access as well */
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+
+	/* Start as soon as a consistent state has been reached in a hot standby */
+	bgw.bgw_start_time = BgWorkerStart_ConsistentState_HotStandby;
+
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "ReplSlotSyncWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "replication slot sync worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN,
+			 "slot sync worker");
+
+	bgw.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
+	bgw.bgw_notify_pid = 0;
+	bgw.bgw_main_arg = (Datum) 0;
+
+	RegisterBackgroundWorker(&bgw);
+}
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 110cb59783..605dfb0426 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -46,7 +46,9 @@
 #include "common/string.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
+#include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -103,7 +105,6 @@ int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
 
 static void ReplicationSlotShmemExit(int code, Datum arg);
-static void ReplicationSlotDropAcquired(void);
 static void ReplicationSlotDropPtr(ReplicationSlot *slot);
 
 /* internal persistency functions */
@@ -250,11 +251,12 @@ ReplicationSlotValidateName(const char *name, int elevel)
  *     user will only get commit prepared.
  * failover: If enabled, allows the slot to be synced to standbys so
  *     that logical replication can be resumed after failover.
+ * synced: True if the slot is created by a slotsync worker.
  */
 void
 ReplicationSlotCreate(const char *name, bool db_specific,
 					  ReplicationSlotPersistency persistency,
-					  bool two_phase, bool failover)
+					  bool two_phase, bool failover, bool synced)
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
@@ -263,6 +265,19 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 
 	ReplicationSlotValidateName(name, ERROR);
 
+	/*
+	 * Do not allow users to create the slots with failover enabled on the
+	 * standby as we do not support sync to the cascading standby.
+	 *
+	 * Slot sync worker can still create slots with failover enabled, as it
+	 * needs to maintain this value in sync with the remote slots.
+	 */
+	if (failover && RecoveryInProgress() && !IsLogicalSlotSyncWorker())
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot enable failover for a replication slot"
+					   " created on the standby"));
+
 	/*
 	 * If some other backend ran this code concurrently with us, we'd likely
 	 * both allocate the same slot, and that would be bad.  We'd also be at
@@ -315,6 +330,7 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 	slot->data.two_phase = two_phase;
 	slot->data.two_phase_at = InvalidXLogRecPtr;
 	slot->data.failover = failover;
+	slot->data.synced = synced;
 
 	/* and then data only present in shared memory */
 	slot->just_dirtied = false;
@@ -677,6 +693,16 @@ ReplicationSlotDrop(const char *name, bool nowait)
 
 	ReplicationSlotAcquire(name, nowait);
 
+	/*
+	 * Do not allow users to drop the slots which are currently being synced
+	 * from the primary to the standby.
+	 */
+	if (RecoveryInProgress() && MyReplicationSlot->data.synced)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot drop replication slot \"%s\"", name),
+				errdetail("This slot is being synced from the primary server."));
+
 	ReplicationSlotDropAcquired();
 }
 
@@ -696,6 +722,29 @@ ReplicationSlotAlter(const char *name, bool failover)
 				errmsg("cannot use %s with a physical replication slot",
 					   "ALTER_REPLICATION_SLOT"));
 
+	if (RecoveryInProgress())
+	{
+		/*
+		 * Do not allow users to alter the slots which are currently being
+		 * synced from the primary to the standby.
+		 */
+		if (MyReplicationSlot->data.synced)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot alter replication slot \"%s\"", name),
+					errdetail("This slot is being synced from the primary server."));
+
+		/*
+		 * Do not allow users to alter slots to enable failover on the standby
+		 * as we do not support sync to the cascading standby.
+		 */
+		if (failover)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot enable failover for a replication slot"
+						   " on the standby"));
+	}
+
 	SpinLockAcquire(&MyReplicationSlot->mutex);
 	MyReplicationSlot->data.failover = failover;
 	SpinLockRelease(&MyReplicationSlot->mutex);
@@ -708,7 +757,7 @@ ReplicationSlotAlter(const char *name, bool failover)
 /*
  * Permanently drop the currently acquired replication slot.
  */
-static void
+void
 ReplicationSlotDropAcquired(void)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
@@ -864,8 +913,8 @@ ReplicationSlotMarkDirty(void)
 }
 
 /*
- * Convert a slot that's marked as RS_EPHEMERAL to a RS_PERSISTENT slot,
- * guaranteeing it will be there after an eventual crash.
+ * Convert a slot that's marked as RS_EPHEMERAL or RS_TEMPORARY to a
+ * RS_PERSISTENT slot, guaranteeing it will be there after an eventual crash.
  */
 void
 ReplicationSlotPersist(void)
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index eb685089b3..9913396152 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -43,7 +43,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
-						  false);
+						  false, false);
 
 	if (immediately_reserve)
 	{
@@ -136,7 +136,7 @@ create_logical_replication_slot(char *name, char *plugin,
 	 */
 	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
-						  failover);
+						  failover, false);
 
 	/*
 	 * Create logical decoding context to find start point or, if we don't
@@ -237,7 +237,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_get_replication_slots(PG_FUNCTION_ARGS)
 {
-#define PG_GET_REPLICATION_SLOTS_COLS 16
+#define PG_GET_REPLICATION_SLOTS_COLS 17
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	XLogRecPtr	currlsn;
 	int			slotno;
@@ -418,21 +418,23 @@ pg_get_replication_slots(PG_FUNCTION_ARGS)
 					break;
 
 				case RS_INVAL_WAL_REMOVED:
-					values[i++] = CStringGetTextDatum("wal_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_REMOVED_TEXT);
 					break;
 
 				case RS_INVAL_HORIZON:
-					values[i++] = CStringGetTextDatum("rows_removed");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_HORIZON_TEXT);
 					break;
 
 				case RS_INVAL_WAL_LEVEL:
-					values[i++] = CStringGetTextDatum("wal_level_insufficient");
+					values[i++] = CStringGetTextDatum(SLOT_INVAL_WAL_LEVEL_TEXT);
 					break;
 			}
 		}
 
 		values[i++] = BoolGetDatum(slot_contents.data.failover);
 
+		values[i++] = BoolGetDatum(slot_contents.data.synced);
+
 		Assert(i == PG_GET_REPLICATION_SLOTS_COLS);
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
@@ -700,7 +702,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	XLogRecPtr	src_restart_lsn;
 	bool		src_islogical;
 	bool		temporary;
-	bool		failover;
 	char	   *plugin;
 	Datum		values[2];
 	bool		nulls[2];
@@ -756,7 +757,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 	src_islogical = SlotIsLogical(&first_slot_contents);
 	src_restart_lsn = first_slot_contents.data.restart_lsn;
 	temporary = (first_slot_contents.data.persistency == RS_TEMPORARY);
-	failover = first_slot_contents.data.failover;
 	plugin = logical_slot ? NameStr(first_slot_contents.data.plugin) : NULL;
 
 	/* Check type of replication slot */
@@ -791,12 +791,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 		 * We must not try to read WAL, since we haven't reserved it yet --
 		 * hence pass find_startpoint false.  confirmed_flush will be set
 		 * below, by copying from the source slot.
+		 *
+		 * To avoid potential issues with the slotsync worker when the
+		 * restart_lsn of a replication slot goes backwards, we set the
+		 * failover option to false here. This situation occurs when a slot on
+		 * the primary server is dropped and immediately replaced with a new
+		 * slot of the same name, created by copying from another existing
+		 * slot. However, the slotsync worker will only observe the
+		 * restart_lsn of the same slot going backwards.
 		 */
 		create_logical_replication_slot(NameStr(*dst_name),
 										plugin,
 										temporary,
 										false,
-										failover,
+										false,
 										src_restart_lsn,
 										false);
 	}
diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c
index 73a7d8f96c..d420a833cd 100644
--- a/src/backend/replication/walreceiverfuncs.c
+++ b/src/backend/replication/walreceiverfuncs.c
@@ -345,6 +345,22 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI)
 	return recptr;
 }
 
+/*
+ * Returns the latest reported end of WAL on the sender
+ */
+XLogRecPtr
+GetWalRcvLatestWalEnd()
+{
+	WalRcvData *walrcv = WalRcv;
+	XLogRecPtr	recptr;
+
+	SpinLockAcquire(&walrcv->mutex);
+	recptr = walrcv->latestWalEnd;
+	SpinLockRelease(&walrcv->mutex);
+
+	return recptr;
+}
+
 /*
  * Returns the last+1 byte position that walreceiver has written.
  * This returns a recently written value without taking a lock.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 77c8baa32a..15f045fe1f 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -72,6 +72,7 @@
 #include "postmaster/interrupt.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
+#include "replication/logicalworker.h"
 #include "replication/slot.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
@@ -243,7 +244,6 @@ static void WalSndShutdown(void) pg_attribute_noreturn();
 static void XLogSendPhysical(void);
 static void XLogSendLogical(void);
 static void WalSndDone(WalSndSendDataCallback send_data);
-static XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 static void IdentifySystem(void);
 static void UploadManifest(void);
 static bool HandleUploadManifestPacket(StringInfo buf, off_t *offset,
@@ -1224,7 +1224,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
 							  cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
-							  false, false);
+							  false, false, false);
 
 		if (reserve_wal)
 		{
@@ -1255,7 +1255,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		 */
 		ReplicationSlotCreate(cmd->slotname, true,
 							  cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
-							  two_phase, failover);
+							  two_phase, failover, false);
 
 		/*
 		 * Do options check early so that we can bail before calling the
@@ -3375,14 +3375,17 @@ WalSndDone(WalSndSendDataCallback send_data)
 }
 
 /*
- * Returns the latest point in WAL that has been safely flushed to disk, and
- * can be sent to the standby. This should only be called when in recovery,
- * ie. we're streaming to a cascaded standby.
+ * Returns the latest point in WAL that has been safely flushed to disk.
+ * This should only be called when in recovery.
+ *
+ * This is called either by cascading walsender to find WAL postion to
+ * be sent to a cascaded standby or by a slot sync worker to validate
+ * remote slot's lsn before syncing it locally.
  *
  * As a side-effect, *tli is updated to the TLI of the last
  * replayed WAL record.
  */
-static XLogRecPtr
+XLogRecPtr
 GetStandbyFlushRecPtr(TimeLineID *tli)
 {
 	XLogRecPtr	replayPtr;
@@ -3391,6 +3394,8 @@ GetStandbyFlushRecPtr(TimeLineID *tli)
 	TimeLineID	receiveTLI;
 	XLogRecPtr	result;
 
+	Assert(am_cascading_walsender || IsLogicalSlotSyncWorker());
+
 	/*
 	 * We can safely send what's already been replayed. Also, if walreceiver
 	 * is streaming WAL from the same timeline, we can send anything that it
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 7084e18861..925ac6c942 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -38,6 +38,7 @@
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	WalSummarizerShmemInit();
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
+	SlotSyncWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715..e8c530acd9 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3286,6 +3286,17 @@ ProcessInterrupts(void)
 			 */
 			proc_exit(1);
 		}
+		else if (IsLogicalSlotSyncWorker())
+		{
+			elog(DEBUG1,
+				 "replication slot sync worker is shutting down due to administrator command");
+
+			/*
+			 * Slot sync worker can be stopped at any time. Use exit status 1
+			 * so the background worker is restarted.
+			 */
+			proc_exit(1);
+		}
 		else if (IsBackgroundWorker)
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a5df835dd4..3e6203322a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -53,6 +53,7 @@ LOGICAL_APPLY_MAIN	"Waiting in main loop of logical replication apply process."
 LOGICAL_LAUNCHER_MAIN	"Waiting in main loop of logical replication launcher process."
 LOGICAL_PARALLEL_APPLY_MAIN	"Waiting in main loop of logical replication parallel apply process."
 RECOVERY_WAL_STREAM	"Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPL_SLOTSYNC_MAIN	"Waiting in main loop of slot sync worker."
 SYSLOGGER_MAIN	"Waiting in main loop of syslogger process."
 WAL_RECEIVER_MAIN	"Waiting in main loop of WAL receiver process."
 WAL_SENDER_MAIN	"Waiting in main loop of WAL sender process."
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7..fe044c16de 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "replication/worker_internal.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -2054,6 +2055,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_syncslot", PGC_POSTMASTER, REPLICATION_STANDBY,
+			gettext_noop("Enables a physical standby to synchronize logical failover slots from the primary server."),
+		},
+		&enable_syncslot,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac..3868694d3f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -361,6 +361,7 @@
 #wal_retrieve_retry_interval = 5s	# time to wait before retrying to
 					# retrieve WAL after a failed attempt
 #recovery_min_apply_delay = 0		# minimum delay for applying changes during recovery
+#enable_syncslot = off			# enables slot synchronization on the physical standby from the primary
 
 # - Subscribers -
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..994e33f59b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11127,9 +11127,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,text,bool}',
-  proargmodes => '{o,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,conflict_reason,failover}',
+  proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,text,bool,bool}',
+  proargmodes => '{o,o,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,conflict_reason,failover,synced}',
   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/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h
index 22fc49ec27..7092fc72c6 100644
--- a/src/include/postmaster/bgworker.h
+++ b/src/include/postmaster/bgworker.h
@@ -79,6 +79,7 @@ typedef enum
 	BgWorkerStart_PostmasterStart,
 	BgWorkerStart_ConsistentState,
 	BgWorkerStart_RecoveryFinished,
+	BgWorkerStart_ConsistentState_HotStandby,
 } BgWorkerStartTime;
 
 #define BGW_DEFAULT_RESTART_INTERVAL	60
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index a18d79d1b2..bbe04226db 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -22,6 +22,7 @@ extern void TablesyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
+extern bool IsLogicalSlotSyncWorker(void);
 
 extern void HandleParallelApplyMessageInterrupt(void);
 extern void HandleParallelApplyMessages(void);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index da4c776492..1f4446aa3a 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -52,6 +52,14 @@ typedef enum ReplicationSlotInvalidationCause
 	RS_INVAL_WAL_LEVEL,
 } ReplicationSlotInvalidationCause;
 
+/*
+ * The possible values for 'conflict_reason' returned in
+ * pg_get_replication_slots.
+ */
+#define SLOT_INVAL_WAL_REMOVED_TEXT "wal_removed"
+#define SLOT_INVAL_HORIZON_TEXT     "rows_removed"
+#define SLOT_INVAL_WAL_LEVEL_TEXT   "wal_level_insufficient"
+
 /*
  * On-Disk data of a replication slot, preserved across restarts.
  */
@@ -112,6 +120,11 @@ typedef struct ReplicationSlotPersistentData
 	/* plugin name */
 	NameData	plugin;
 
+	/*
+	 * Was this slot synchronized from the primary server?
+	 */
+	char		synced;
+
 	/*
 	 * Is this a failover slot (sync candidate for standbys)? Only relevant
 	 * for logical slots on the primary server.
@@ -224,9 +237,11 @@ extern void ReplicationSlotsShmemInit(void);
 /* management of individual slots */
 extern void ReplicationSlotCreate(const char *name, bool db_specific,
 								  ReplicationSlotPersistency persistency,
-								  bool two_phase, bool failover);
+								  bool two_phase, bool failover,
+								  bool synced);
 extern void ReplicationSlotPersist(void);
 extern void ReplicationSlotDrop(const char *name, bool nowait);
+extern void ReplicationSlotDropAcquired(void);
 extern void ReplicationSlotAlter(const char *name, bool failover);
 
 extern void ReplicationSlotAcquire(const char *name, bool nowait);
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index f566a99ba1..48dc846e19 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -279,6 +279,13 @@ typedef void (*walrcv_get_senderinfo_fn) (WalReceiverConn *conn,
 typedef char *(*walrcv_identify_system_fn) (WalReceiverConn *conn,
 											TimeLineID *primary_tli);
 
+/*
+ * walrcv_get_dbname_from_conninfo_fn
+ *
+ * Returns the dbid from the primary_conninfo
+ */
+typedef char *(*walrcv_get_dbname_from_conninfo_fn) (const char *conninfo);
+
 /*
  * walrcv_server_version_fn
  *
@@ -403,6 +410,7 @@ typedef struct WalReceiverFunctionsType
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
+	walrcv_get_dbname_from_conninfo_fn walrcv_get_dbname_from_conninfo;
 	walrcv_server_version_fn walrcv_server_version;
 	walrcv_readtimelinehistoryfile_fn walrcv_readtimelinehistoryfile;
 	walrcv_startstreaming_fn walrcv_startstreaming;
@@ -428,6 +436,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_get_senderinfo(conn, sender_host, sender_port)
 #define walrcv_identify_system(conn, primary_tli) \
 	WalReceiverFunctions->walrcv_identify_system(conn, primary_tli)
+#define walrcv_get_dbname_from_conninfo(conninfo) \
+	WalReceiverFunctions->walrcv_get_dbname_from_conninfo(conninfo)
 #define walrcv_server_version(conn) \
 	WalReceiverFunctions->walrcv_server_version(conn)
 #define walrcv_readtimelinehistoryfile(conn, tli, filename, content, size) \
@@ -485,6 +495,7 @@ extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr,
 								 bool create_temp_slot);
 extern XLogRecPtr GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI);
 extern XLogRecPtr GetWalRcvWriteRecPtr(void);
+extern XLogRecPtr GetWalRcvLatestWalEnd(void);
 extern int	GetReplicationApplyDelay(void);
 extern int	GetReplicationTransferLatency(void);
 
diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h
index 1b58d50b3b..276d8913aa 100644
--- a/src/include/replication/walsender.h
+++ b/src/include/replication/walsender.h
@@ -12,6 +12,8 @@
 #ifndef _WALSENDER_H
 #define _WALSENDER_H
 
+#include "access/xlogdefs.h"
+
 /*
  * What to do with a snapshot in create replication slot command.
  */
@@ -45,6 +47,7 @@ extern void WalSndInitStopping(void);
 extern void WalSndWaitStopping(void);
 extern void HandleWalSndInitStopping(void);
 extern void WalSndRqstFileReload(void);
+extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli);
 
 /*
  * Remember that we want to wakeup walsenders later
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 515aefd519..2167720971 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -237,6 +237,11 @@ extern PGDLLIMPORT bool in_remote_transaction;
 
 extern PGDLLIMPORT bool InitializingApplyWorker;
 
+/* Slot sync worker objects */
+extern PGDLLIMPORT char *PrimaryConnInfo;
+extern PGDLLIMPORT char *PrimarySlotName;
+extern PGDLLIMPORT bool enable_syncslot;
+
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
 												bool only_running);
@@ -325,6 +330,11 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+extern void ReplSlotSyncWorkerMain(Datum main_arg);
+extern void SlotSyncWorkerRegister(void);
+extern void ShutDownSlotSync(void);
+extern void SlotSyncWorkerShmemInit(void);
+
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
 #define isTablesyncWorker(worker) ((worker)->in_use && \
diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl
index bc58ff4cab..2a017b4286 100644
--- a/src/test/recovery/t/040_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl
@@ -18,7 +18,8 @@ $publisher->init(allows_streaming => 'logical');
 $publisher->start;
 
 $publisher->safe_psql('postgres',
-	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;");
+	"CREATE PUBLICATION regress_mypub FOR ALL TABLES;"
+);
 
 my $publisher_connstr = $publisher->connstr . ' dbname=postgres';
 
@@ -97,4 +98,168 @@ my ($result, $stdout, $stderr) = $subscriber1->psql('postgres',
 ok( $stderr =~ /ERROR:  cannot set failover for enabled subscription/,
 	"altering failover is not allowed for enabled subscription");
 
+##################################################
+# Test logical failover slots on the standby
+# Configure standby1 to replicate and synchronize logical slots configured
+# for failover on the primary
+#
+#              failover slot lsub1_slot->| ----> subscriber1 (connected via logical replication)
+# primary --->                           |
+#              physical slot sb1_slot--->| ----> standby1 (connected via streaming replication)
+#                                        |                 lsub1_slot(synced_slot)
+##################################################
+
+my $primary = $publisher;
+my $backup_name = 'backup';
+$primary->backup($backup_name);
+
+# Create a standby
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+	$primary, $backup_name,
+	has_streaming => 1,
+	has_restoring => 1);
+
+my $connstr_1 = $primary->connstr;
+$standby1->append_conf(
+	'postgresql.conf', qq(
+enable_syncslot = true
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr_1 dbname=postgres'
+));
+
+$primary->psql('postgres',
+	q{SELECT pg_create_physical_replication_slot('sb1_slot');});
+
+my $standby1_conninfo = $standby1->connstr . ' dbname=postgres';
+my $offset = -s $standby1->logfile;
+
+# Start the standby so that slot syncing can begin
+$standby1->start;
+
+# Generate a log to trigger the walsender to send messages to the walreceiver
+# which will update WalRcv->latestWalEnd to a valid number.
+$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();");
+
+# Wait for the standby to finish sync
+$standby1->wait_for_log(
+	qr/LOG: ( [A-Z0-9]+:)? newly created slot \"lsub1_slot\" is sync-ready now/,
+	$offset);
+
+# Confirm that the logical failover slot is created on the standby and is
+# flagged as 'synced'
+is($standby1->safe_psql('postgres',
+	q{SELECT synced FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	"t",
+	'logical slot has synced as true on standby');
+
+##################################################
+# Test to confirm that restart_lsn and confirmed_flush_lsn of the logical slot
+# on the primary is synced to the standby
+##################################################
+
+# Insert data on the primary
+$primary->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	INSERT INTO tab_int SELECT generate_series(1, 10);
+]);
+
+# Subscribe to the new table data and wait for it to arrive
+$subscriber1->safe_psql(
+	'postgres', qq[
+	CREATE TABLE tab_int (a int PRIMARY KEY);
+	ALTER SUBSCRIPTION regress_mysub1 REFRESH PUBLICATION;
+]);
+
+$subscriber1->wait_for_subscription_sync;
+
+# Do not allow any further advancement of the restart_lsn and
+# confirmed_flush_lsn for the lsub1_slot.
+$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+
+# Wait for the replication slot to become inactive on the publisher
+$primary->poll_query_until(
+	'postgres',
+	"SELECT COUNT(*) FROM pg_catalog.pg_replication_slots WHERE slot_name = 'lsub1_slot' AND active='f'",
+	1);
+
+# Get the restart_lsn for the logical slot lsub1_slot on the primary
+my $primary_restart_lsn = $primary->safe_psql('postgres',
+	"SELECT restart_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Get the confirmed_flush_lsn for the logical slot lsub1_slot on the primary
+my $primary_flush_lsn = $primary->safe_psql('postgres',
+	"SELECT confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';");
+
+# Confirm that restart_lsn and of confirmed_flush_lsn lsub1_slot slot are synced
+# to the standby
+ok( $standby1->poll_query_until(
+		'postgres',
+		"SELECT '$primary_restart_lsn' = restart_lsn AND '$primary_flush_lsn' = confirmed_flush_lsn from pg_replication_slots WHERE slot_name = 'lsub1_slot';"),
+	'restart_lsn and confirmed_flush_lsn of slot lsub1_slot synced to standby');
+
+##################################################
+# Test that a synchronized slot can not be decoded, altered or dropped by the user
+##################################################
+
+# Disable hot_standby_feedback temporarily to stop slot sync worker otherwise
+# the concerned testing scenarios here may be interrupted by different error:
+# 'ERROR:  replication slot is active for PID ..'
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = off;');
+$standby1->restart;
+
+# Attempting to perform logical decoding on a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"select * from pg_logical_slot_get_changes('lsub1_slot',NULL,NULL);");
+ok($stderr =~ /ERROR:  cannot use replication slot "lsub1_slot" for logical decoding/,
+	"logical decoding is not allowed on synced slot");
+
+# Attempting to alter a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql(
+    'postgres',
+    qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);],
+    replication => 'database');
+ok($stderr =~ /ERROR:  cannot alter replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be altered");
+
+# Attempting to drop a synced slot should result in an error
+($result, $stdout, $stderr) = $standby1->psql('postgres',
+	"SELECT pg_drop_replication_slot('lsub1_slot');");
+ok($stderr =~ /ERROR:  cannot drop replication slot "lsub1_slot"/,
+	"synced slot on standby cannot be dropped");
+
+# Enable hot_standby_feedback and restart standby
+$standby1->safe_psql('postgres', 'ALTER SYSTEM SET hot_standby_feedback = on;');
+$standby1->restart;
+
+##################################################
+# Promote the standby1 to primary. Confirm that:
+# a) the slot 'lsub1_slot' is retained on the new primary
+# b) logical replication for regress_mysub1 is resumed successfully after failover
+##################################################
+$standby1->promote;
+
+# Update subscription with the new primary's connection info
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 CONNECTION '$standby1_conninfo';
+	 ALTER SUBSCRIPTION regress_mysub1 ENABLE; ");
+
+# Confirm the synced slot 'lsub1_slot' is retained on the new primary
+is($standby1->safe_psql('postgres',
+	q{SELECT slot_name FROM pg_replication_slots WHERE slot_name = 'lsub1_slot';}),
+	'lsub1_slot',
+	'synced slot retained on the new primary');
+
+# Insert data on the new primary
+$standby1->safe_psql('postgres',
+	"INSERT INTO tab_int SELECT generate_series(11, 20);");
+$standby1->wait_for_catchup('regress_mysub1');
+
+# Confirm that data in tab_int replicated on the subscriber
+is( $subscriber1->safe_psql('postgres', q{SELECT count(*) FROM tab_int;}),
+	"20",
+	'data replicated from the new primary');
+
 done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index abc944e8b8..b7488d760e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1474,8 +1474,9 @@ pg_replication_slots| SELECT l.slot_name,
     l.safe_wal_size,
     l.two_phase,
     l.conflict_reason,
-    l.failover
-   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, conflict_reason, failover)
+    l.failover,
+    l.synced
+   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, conflict_reason, failover, synced)
      LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
 pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolsuper,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 9be7aca2b8..fae059d389 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -133,8 +133,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_self_join_removal       | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_syncslot                | off
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.
 select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90b37b919c..62554d527e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2325,6 +2325,7 @@ RelocationBufferInfo
 RelptrFreePageBtree
 RelptrFreePageManager
 RelptrFreePageSpanLeader
+RemoteSlot
 RenameStmt
 ReopenPtrType
 ReorderBuffer
@@ -2584,6 +2585,7 @@ SlabBlock
 SlabContext
 SlabSlot
 SlotNumber
+SlotSyncWorkerCtxStruct
 SlruCtl
 SlruCtlData
 SlruErrorCause
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-30 15:45  Bertrand Drouvot <[email protected]>
  parent: Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Bertrand Drouvot @ 2024-01-30 15:45 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Peter Smith <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Hi,

On Mon, Jan 29, 2024 at 09:15:57AM +0000, Bertrand Drouvot wrote:
> Hi,
> 
> On Mon, Jan 29, 2024 at 02:35:52PM +0530, Amit Kapila wrote:
> > I think it is better to create a separate patch for two_phase after
> > this patch gets committed.
> 
> Yeah, makes sense, will do, thanks!

It's done in [1].

[1]: https://www.postgresql.org/message-id/ZbkYrLPhH%2BRxpZlW%40ip-10-97-1-34.eu-west-3.compute.internal

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-31 01:57  Peter Smith <[email protected]>
  parent: shveta malik <[email protected]>
  1 sibling, 2 replies; 119+ messages in thread

From: Peter Smith @ 2024-01-31 01:57 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

Hi,

I saw that v73-0001 was pushed, but it included some last-minute
changes that I did not get a chance to check yesterday.

Here are some review comments for the new parts of that patch.

======
doc/src/sgml/ref/create_subscription.sgml

1.
connect (boolean)

    Specifies whether the CREATE SUBSCRIPTION command should connect
to the publisher at all. The default is true. Setting this to false
will force the values of create_slot, enabled, copy_data, and failover
to false. (You cannot combine setting connect to false with setting
create_slot, enabled, copy_data, or failover to true.)

~

I don't think the first part "Setting this to false will force the
values ... failover to false." is strictly correct.

I think is correct to say all those *other* properties (create_slot,
enabled, copy_data) are forced to false because those otherwise have
default true values. But the 'failover' has default false, so it
cannot get force-changed at all because you can't set connect to false
when failover is true as the second part ("You cannot combine...")
explains.

IMO remove 'failover' from that first sentence.

~~~

2.
          <para>
           Since no connection is made when this option is
           <literal>false</literal>, no tables are subscribed. To initiate
           replication, you must manually create the replication slot, enable
-          the subscription, and refresh the subscription. See
+          the failover if required, enable the subscription, and refresh the
+          subscription. See
           <xref
linkend="logical-replication-subscription-examples-deferred-slot"/>
           for examples.
          </para>

IMO "see the failover if required" is very vague. See what failover?
The slot property failover, or the subscription option failover? And
"see" it for what purpose?

I think the intention was probably to say something like "ensure the
manually created slot has the same matching failover property value as
the subscriber failover option", but that is not clear from the
current text.

======
doc/src/sgml/ref/pg_dump.sgml

3.
    dump can be restored without requiring network access to the remote
    servers.  It is then up to the user to reactivate the subscriptions in a
    suitable way.  If the involved hosts have changed, the connection
-   information might have to be changed.  It might also be appropriate to
+   information might have to be changed.  If the subscription needs to
+   be enabled for
+   <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>,
+   then same needs to be done by executing
+   <link linkend="sql-altersubscription-params-set">
+   <literal>ALTER SUBSCRIPTION ... SET(failover = true)</literal></link>
+   after the slot has been created.  It might also be appropriate to

"then same needs to be done" (English?)

BEFORE
If the subscription needs to be enabled for failover, then same needs
to be done by executing ALTER SUBSCRIPTION ... SET(failover = true)
after the slot has been created.

SUGGESTION
If the subscription needs to be enabled for failover, execute ALTER
SUBSCRIPTION ... SET(failover = true) after the slot has been created.

======
src/backend/commands/subscriptioncmds.c

4.
 #define SUBOPT_RUN_AS_OWNER 0x00001000
-#define SUBOPT_LSN 0x00002000
-#define SUBOPT_ORIGIN 0x00004000
+#define SUBOPT_FAILOVER 0x00002000
+#define SUBOPT_LSN 0x00004000
+#define SUBOPT_ORIGIN 0x00008000
+

A spurious blank line was added.

======
src/bin/pg_upgrade/t/004_subscription.pl

5.
-# The subscription's running status should be preserved. Old subscription
-# regress_sub1 should be enabled and old subscription regress_sub2 should be
-# disabled.
+# The subscription's running status and failover option should be preserved.
+# Old subscription regress_sub1 should have enabled and failover as true while
+# old subscription regress_sub2 should have enabled and failover as false.
 $result =
   $new_sub->safe_psql('postgres',
- "SELECT subname, subenabled FROM pg_subscription ORDER BY subname");
-is( $result, qq(regress_sub1|t
-regress_sub2|f),
+ "SELECT subname, subenabled, subfailover FROM pg_subscription ORDER
BY subname");
+is( $result, qq(regress_sub1|t|t
+regress_sub2|f|f),
  "check that the subscription's running status are preserved");

~

Calling those "old subscriptions" seems misleading. Aren't these the
new/upgraded subscriptions being checked here?

Should the comment be more like:

# The subscription's running status and failover option should be preserved.
# Upgraded regress_sub1 should still have enabled and failover as true.
# Upgraded regress_sub2 should still have enabled and failover as false.

======
Kind Regards,
Peter Smith.
Fujitsu Australia.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-31 03:18  Amit Kapila <[email protected]>
  parent: Peter Smith <[email protected]>
  1 sibling, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-31 03:18 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Wed, Jan 31, 2024 at 7:27 AM Peter Smith <[email protected]> wrote:
>
> I saw that v73-0001 was pushed, but it included some last-minute
> changes that I did not get a chance to check yesterday.
>
> Here are some review comments for the new parts of that patch.
>
> ======
> doc/src/sgml/ref/create_subscription.sgml
>
> 1.
> connect (boolean)
>
>     Specifies whether the CREATE SUBSCRIPTION command should connect
> to the publisher at all. The default is true. Setting this to false
> will force the values of create_slot, enabled, copy_data, and failover
> to false. (You cannot combine setting connect to false with setting
> create_slot, enabled, copy_data, or failover to true.)
>
> ~
>
> I don't think the first part "Setting this to false will force the
> values ... failover to false." is strictly correct.
>
> I think is correct to say all those *other* properties (create_slot,
> enabled, copy_data) are forced to false because those otherwise have
> default true values.
>

So, won't when connect=false, the user has to explicitly provide such
values (create_slot, enabled, etc.) as false? If so, is using 'force'
strictly correct?

> ~~~
>
> 2.
>           <para>
>            Since no connection is made when this option is
>            <literal>false</literal>, no tables are subscribed. To initiate
>            replication, you must manually create the replication slot, enable
> -          the subscription, and refresh the subscription. See
> +          the failover if required, enable the subscription, and refresh the
> +          subscription. See
>            <xref
> linkend="logical-replication-subscription-examples-deferred-slot"/>
>            for examples.
>           </para>
>
> IMO "see the failover if required" is very vague. See what failover?
>

AFAICS, the committed docs says: "To initiate replication, you must
manually create the replication slot, enable the failover if required,
...". I am not sure what you are referring to.

>
> ======
> src/bin/pg_upgrade/t/004_subscription.pl
>
> 5.
> -# The subscription's running status should be preserved. Old subscription
> -# regress_sub1 should be enabled and old subscription regress_sub2 should be
> -# disabled.
> +# The subscription's running status and failover option should be preserved.
> +# Old subscription regress_sub1 should have enabled and failover as true while
> +# old subscription regress_sub2 should have enabled and failover as false.
>  $result =
>    $new_sub->safe_psql('postgres',
> - "SELECT subname, subenabled FROM pg_subscription ORDER BY subname");
> -is( $result, qq(regress_sub1|t
> -regress_sub2|f),
> + "SELECT subname, subenabled, subfailover FROM pg_subscription ORDER
> BY subname");
> +is( $result, qq(regress_sub1|t|t
> +regress_sub2|f|f),
>   "check that the subscription's running status are preserved");
>
> ~
>
> Calling those "old subscriptions" seems misleading. Aren't these the
> new/upgraded subscriptions being checked here?
>

Again the quoted wording is not introduced by this patch. But, I see
your point and it is better if you can start a separate thread for it.

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-31 05:10  Peter Smith <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Peter Smith @ 2024-01-31 05:10 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Wed, Jan 31, 2024 at 2:18 PM Amit Kapila <[email protected]> wrote:
>
> On Wed, Jan 31, 2024 at 7:27 AM Peter Smith <[email protected]> wrote:
> >
> > I saw that v73-0001 was pushed, but it included some last-minute
> > changes that I did not get a chance to check yesterday.
> >
> > Here are some review comments for the new parts of that patch.
> >
> > ======
> > doc/src/sgml/ref/create_subscription.sgml
> >
> > 1.
> > connect (boolean)
> >
> >     Specifies whether the CREATE SUBSCRIPTION command should connect
> > to the publisher at all. The default is true. Setting this to false
> > will force the values of create_slot, enabled, copy_data, and failover
> > to false. (You cannot combine setting connect to false with setting
> > create_slot, enabled, copy_data, or failover to true.)
> >
> > ~
> >
> > I don't think the first part "Setting this to false will force the
> > values ... failover to false." is strictly correct.
> >
> > I think is correct to say all those *other* properties (create_slot,
> > enabled, copy_data) are forced to false because those otherwise have
> > default true values.
> >
>
> So, won't when connect=false, the user has to explicitly provide such
> values (create_slot, enabled, etc.) as false? If so, is using 'force'
> strictly correct?

Perhaps the original docs text could be worded differently; I think
the word "force" here just meant setting connection=false
forces/causes/makes those other options behave "as if" they had been
set to false without the user explicitly doing anything to them. The
point is they are made to behave *differently* to their normal
defaults.

So,
connect=false ==> this actually sets enabled=false (you can see this
with \dRs+), which is different to the default setting of 'enabled'
connect=false ==> will not create a slot (because there is no
connection), which is different to the default behaviour for
'create-slot'
connect=false ==> will not copy tables (because there is no
connection), which is different to the default behaviour for
'copy_data;'

OTOH, failover is different
connect=false ==> failover is not possible (because there is no
connection), but the 'failover' default is false anyway, so no change
to the default behaviour for this one.

>
> > ~~~
> >
> > 2.
> >           <para>
> >            Since no connection is made when this option is
> >            <literal>false</literal>, no tables are subscribed. To initiate
> >            replication, you must manually create the replication slot, enable
> > -          the subscription, and refresh the subscription. See
> > +          the failover if required, enable the subscription, and refresh the
> > +          subscription. See
> >            <xref
> > linkend="logical-replication-subscription-examples-deferred-slot"/>
> >            for examples.
> >           </para>
> >
> > IMO "see the failover if required" is very vague. See what failover?
> >
>
> AFAICS, the committed docs says: "To initiate replication, you must
> manually create the replication slot, enable the failover if required,
> ...". I am not sure what you are referring to.
>

My mistake. I was misreading the patch code without applying the
patch. Sorry for the noise.

> >
> > ======
> > src/bin/pg_upgrade/t/004_subscription.pl
> >
> > 5.
> > -# The subscription's running status should be preserved. Old subscription
> > -# regress_sub1 should be enabled and old subscription regress_sub2 should be
> > -# disabled.
> > +# The subscription's running status and failover option should be preserved.
> > +# Old subscription regress_sub1 should have enabled and failover as true while
> > +# old subscription regress_sub2 should have enabled and failover as false.
> >  $result =
> >    $new_sub->safe_psql('postgres',
> > - "SELECT subname, subenabled FROM pg_subscription ORDER BY subname");
> > -is( $result, qq(regress_sub1|t
> > -regress_sub2|f),
> > + "SELECT subname, subenabled, subfailover FROM pg_subscription ORDER
> > BY subname");
> > +is( $result, qq(regress_sub1|t|t
> > +regress_sub2|f|f),
> >   "check that the subscription's running status are preserved");
> >
> > ~
> >
> > Calling those "old subscriptions" seems misleading. Aren't these the
> > new/upgraded subscriptions being checked here?
> >
>
> Again the quoted wording is not introduced by this patch. But, I see
> your point and it is better if you can start a separate thread for it.
>

OK. I created a separate thread for this [1]

======
[1] https://www.postgresql.org/message-id/[email protected]...

Kind Regards,
Peter Smith.
Fujitsu Australia.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* RE: Synchronizing slots from primary to standby
@ 2024-01-31 07:23  Zhijie Hou (Fujitsu) <[email protected]>
  parent: Peter Smith <[email protected]>
  1 sibling, 0 replies; 119+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2024-01-31 07:23 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Masahiko Sawada <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Wednesday, January 31, 2024 9:57 AM Peter Smith <[email protected]> wrote:
> 
> Hi,
> 
> I saw that v73-0001 was pushed, but it included some last-minute
> changes that I did not get a chance to check yesterday.
> 
> Here are some review comments for the new parts of that patch.
> 
> ======
> doc/src/sgml/ref/create_subscription.sgml
> 
> 1.
> connect (boolean)
> 
>     Specifies whether the CREATE SUBSCRIPTION command should connect
> to the publisher at all. The default is true. Setting this to false
> will force the values of create_slot, enabled, copy_data, and failover
> to false. (You cannot combine setting connect to false with setting
> create_slot, enabled, copy_data, or failover to true.)
> 
> ~
> 
> I don't think the first part "Setting this to false will force the
> values ... failover to false." is strictly correct.
> 
> I think is correct to say all those *other* properties (create_slot,
> enabled, copy_data) are forced to false because those otherwise have
> default true values. But the 'failover' has default false, so it
> cannot get force-changed at all because you can't set connect to false
> when failover is true as the second part ("You cannot combine...")
> explains.
> 
> IMO remove 'failover' from that first sentence.
> 
> 
> 3.
>     dump can be restored without requiring network access to the remote
>     servers.  It is then up to the user to reactivate the subscriptions in a
>     suitable way.  If the involved hosts have changed, the connection
> -   information might have to be changed.  It might also be appropriate to
> +   information might have to be changed.  If the subscription needs to
> +   be enabled for
> +   <link
> linkend="sql-createsubscription-params-with-failover"><literal>failover</lit
> eral></link>,
> +   then same needs to be done by executing
> +   <link linkend="sql-altersubscription-params-set">
> +   <literal>ALTER SUBSCRIPTION ... SET(failover = true)</literal></link>
> +   after the slot has been created.  It might also be appropriate to
> 
> "then same needs to be done" (English?)
> 
> BEFORE
> If the subscription needs to be enabled for failover, then same needs
> to be done by executing ALTER SUBSCRIPTION ... SET(failover = true)
> after the slot has been created.
> 
> SUGGESTION
> If the subscription needs to be enabled for failover, execute ALTER
> SUBSCRIPTION ... SET(failover = true) after the slot has been created.
> 
> ======
> src/backend/commands/subscriptioncmds.c
> 
> 4.
>  #define SUBOPT_RUN_AS_OWNER 0x00001000
> -#define SUBOPT_LSN 0x00002000
> -#define SUBOPT_ORIGIN 0x00004000
> +#define SUBOPT_FAILOVER 0x00002000
> +#define SUBOPT_LSN 0x00004000
> +#define SUBOPT_ORIGIN 0x00008000
> +
> 
> A spurious blank line was added.
> 

Here is a small patch to address the comment 3 and 4.
The discussion for comment 1 is still going on, so we can
update the patch once it's concluded.

Best Regards,
Hou zj



Attachments:

  [application/octet-stream] 0001-clean-up-for-776621a5.patch (1.8K, ../../OS0PR01MB5716D24EA2E581675335A4D7947C2@OS0PR01MB5716.jpnprd01.prod.outlook.com/2-0001-clean-up-for-776621a5.patch)
  download | inline diff:
From 1526da8444651c4ea1663fa27bc7c15c04326f92 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <[email protected]>
Date: Wed, 31 Jan 2024 11:25:25 +0800
Subject: [PATCH] clean up for 776621a5

Improve the document in pg_dump and remove one spurious blank line.

---
 doc/src/sgml/ref/pg_dump.sgml           | 4 +---
 src/backend/commands/subscriptioncmds.c | 1 -
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index f8ae4220e1..0caf56e0e0 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1591,9 +1591,7 @@ CREATE DATABASE foo WITH TEMPLATE template0;
    information might have to be changed.  If the subscription needs to
    be enabled for
    <link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>,
-   then same needs to be done by executing
-   <link linkend="sql-altersubscription-params-set">
-   <literal>ALTER SUBSCRIPTION ... SET (failover = true)</literal></link>
+   execute <link linkend="sql-altersubscription-params-set"><literal>ALTER SUBSCRIPTION ... SET (failover = true)</literal></link>
    after the slot has been created.  It might also be appropriate to
    truncate the target tables before initiating a new full table copy.  If users
    intend to copy initial data during refresh they must create the slot with
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index b647a81fc8..420833644a 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -73,7 +73,6 @@
 #define SUBOPT_LSN					0x00004000
 #define SUBOPT_ORIGIN				0x00008000
 
-
 /* check if the 'val' has 'bits' set */
 #define IsSet(val, bits)  (((val) & (bits)) == (bits))
 
-- 
2.30.0.windows.2



^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-31 08:32  Masahiko Sawada <[email protected]>
  parent: shveta malik <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-31 08:32 UTC (permalink / raw)
  To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Tue, Jan 30, 2024 at 9:53 PM shveta malik <[email protected]> wrote:
>
> On Tue, Jan 30, 2024 at 4:06 PM shveta malik <[email protected]> wrote:
> >
> > PFA v73-0001 which addresses the above comments. Other patches will be
> > rebased and posted after pushing this one.
>
> Since v73-0001 is pushed, PFA  rest of the patches. Changes are:
>
> 1) Rebased the patches.
> 2) Ran pg_indent on all.
> 3) patch001: Updated logicaldecoding.sgml for dbname requirement in
> primary_conninfo for slot-synchronization.
>

Thank you for updating the patches. As for the slotsync worker patch,
is there any reason why 0001, 0002, and 0004 patches are still
separated?

Beside, here are some comments on v74 0001, 0002, and 0004 patches:

---
+static char *
+wait_for_valid_params_and_get_dbname(void)
+{
+   char       *dbname;
+   int         rc;
+
+   /* Sanity check. */
+   Assert(enable_syncslot);
+
+   for (;;)
+   {
+       if (validate_parameters_and_get_dbname(&dbname))
+           break;
+       ereport(LOG, errmsg("skipping slot synchronization"));
+
+       ProcessSlotSyncInterrupts(NULL);

When reading this function, I expected that the slotsync worker would
resume working once the parameters became valid, but it was not
correct. For example, if I changed hot_standby_feedback from off to
on, the slotsync worker reads the config file, exits, and then
restarts. Given that the slotsync worker ends up exiting on parameter
changes anyway, why do we want to have it wait for parameters to
become valid? IIUC even if the slotsync worker exits when a parameter
is not valid, it restarts at some intervals.

---
+bool
+SlotSyncWorkerCanRestart(void)
+{
+#define SLOTSYNC_RESTART_INTERVAL_SEC 10
+

IIUC depending on how busy the postmaster is and the timing, the user
could wait for 1 min to re-launch the slotsync worker. But I think the
user might want to re-launch the slotsync worker more quickly for
example when the slotsync worker restarts due to parameter changes.
IIUC SloSyncWorkerCanRestart() doesn't consider the fact that the
slotsync worker previously exited with 0 or 1.

---
+       /* We are a normal standby */
+       valid = DatumGetBool(slot_getattr(tupslot, 2, &isnull));
+       Assert(!isnull);

What do you mean by "normal standby"?

---
+   appendStringInfo(&cmd,
+                    "SELECT pg_is_in_recovery(), count(*) = 1"
+                    " FROM pg_replication_slots"
+                    " WHERE slot_type='physical' AND slot_name=%s",
+                    quote_literal_cstr(PrimarySlotName));

I think we need to make "pg_replication_slots" schema-qualified.

---
+                   errdetail("The primary server slot \"%s\" specified by"
+                             " \"%s\" is not valid.",
+                             PrimarySlotName, "primary_slot_name"));

and

+               errmsg("slot sync worker will shutdown because"
+                      " %s is disabled", "enable_syncslot"));

It's better to write it in one line for better greppability.

---
When I dropped a database on the primary that has a failover slot, I
got the following logs on the standby:

2024-01-31 17:25:21.750 JST [1103933] FATAL:  replication slot "s" is
active for PID 1103935
2024-01-31 17:25:21.750 JST [1103933] CONTEXT:  WAL redo at 0/3020D20
for Database/DROP: dir 1663/16384
2024-01-31 17:25:21.751 JST [1103930] LOG:  startup process (PID
1103933) exited with exit code 1

It seems that because the slotsync worker created the slot on the
standby, the slot's active_pid is still valid. That is why the startup
process could not drop the slot.

Regards,


--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-31 10:42  Amit Kapila <[email protected]>
  parent: Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 119+ messages in thread

From: Amit Kapila @ 2024-01-31 10:42 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Wed, Jan 31, 2024 at 2:02 PM Masahiko Sawada <[email protected]> wrote:
>
> Thank you for updating the patches. As for the slotsync worker patch,
> is there any reason why 0001, 0002, and 0004 patches are still
> separated?
>

No specific reason, it could be easier to review those parts.

>
> Beside, here are some comments on v74 0001, 0002, and 0004 patches:
>
> ---
> +static char *
> +wait_for_valid_params_and_get_dbname(void)
> +{
> +   char       *dbname;
> +   int         rc;
> +
> +   /* Sanity check. */
> +   Assert(enable_syncslot);
> +
> +   for (;;)
> +   {
> +       if (validate_parameters_and_get_dbname(&dbname))
> +           break;
> +       ereport(LOG, errmsg("skipping slot synchronization"));
> +
> +       ProcessSlotSyncInterrupts(NULL);
>
> When reading this function, I expected that the slotsync worker would
> resume working once the parameters became valid, but it was not
> correct. For example, if I changed hot_standby_feedback from off to
> on, the slotsync worker reads the config file, exits, and then
> restarts. Given that the slotsync worker ends up exiting on parameter
> changes anyway, why do we want to have it wait for parameters to
> become valid?
>

Right, the reason for waiting is to avoid repeated re-start of
slotsync worker if the required parameter is not changed. To follow
that, I think we should simply continue when the required parameter is
changed and is valid. But, I think during actual slotsync, if
connection_info is changed then there is no option but to restart.

>
> ---
> +bool
> +SlotSyncWorkerCanRestart(void)
> +{
> +#define SLOTSYNC_RESTART_INTERVAL_SEC 10
> +
>
> IIUC depending on how busy the postmaster is and the timing, the user
> could wait for 1 min to re-launch the slotsync worker. But I think the
> user might want to re-launch the slotsync worker more quickly for
> example when the slotsync worker restarts due to parameter changes.
> IIUC SloSyncWorkerCanRestart() doesn't consider the fact that the
> slotsync worker previously exited with 0 or 1.
>

Considering my previous where we don't want to restart for a required
parameter change, isn't it better to avoid repeated restart (say when
the user gave an invalid dbname)? BTW, I think this restart interval
is added based on your previous complaint [1].

>
> ---
> When I dropped a database on the primary that has a failover slot, I
> got the following logs on the standby:
>
> 2024-01-31 17:25:21.750 JST [1103933] FATAL:  replication slot "s" is
> active for PID 1103935
> 2024-01-31 17:25:21.750 JST [1103933] CONTEXT:  WAL redo at 0/3020D20
> for Database/DROP: dir 1663/16384
> 2024-01-31 17:25:21.751 JST [1103930] LOG:  startup process (PID
> 1103933) exited with exit code 1
>
> It seems that because the slotsync worker created the slot on the
> standby, the slot's active_pid is still valid.
>

But we release the slot after sync. And we do take a shared lock on
the database to make the startup process wait for slotsync. There is
one gap which is that we don't reset active_pid for temp slots in
ReplicationSlotRelease(), so for temp slots such an error can occur
but OTOH, we immediately make the slot persistent after sync. As per
my understanding, it is only possible to get this error if the initial
sync doesn't happen and the slot remains temporary. Is that your case?
How did reproduce this?

 That is why the startup
> process could not drop the slot.
>

[1] - https://www.postgresql.org/message-id/CAD21AoApGoTZu7D_7%3DbVYQqKnj%2BPZ2Rz%2Bnc8Ky1HPQMS_XL6%2BA%40...

-- 
With Regards,
Amit Kapila.





^ permalink  raw  reply  [nested|flat] 119+ messages in thread

* Re: Synchronizing slots from primary to standby
@ 2024-01-31 15:49  Masahiko Sawada <[email protected]>
  parent: Amit Kapila <[email protected]>
  0 siblings, 0 replies; 119+ messages in thread

From: Masahiko Sawada @ 2024-01-31 15:49 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: shveta malik <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Bertrand Drouvot <[email protected]>; Ajin Cherian <[email protected]>; Dilip Kumar <[email protected]>; Nisha Moond <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Bharath Rupireddy <[email protected]>; Peter Eisentraut <[email protected]>; Bruce Momjian <[email protected]>; Ashutosh Sharma <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers; Alvaro Herrera <[email protected]>

On Wed, Jan 31, 2024 at 7:42 PM Amit Kapila <[email protected]> wrote:
>
> On Wed, Jan 31, 2024 at 2:02 PM Masahiko Sawada <[email protected]> wrote:
> >
> > Thank you for updating the patches. As for the slotsync worker patch,
> > is there any reason why 0001, 0002, and 0004 patches are still
> > separated?
> >
>
> No specific reason, it could be easier to review those parts.

Okay, I think we can merge 0001 and 0002 at least as we don't need
bgworker codes.

>
> >
> > Beside, here are some comments on v74 0001, 0002, and 0004 patches:
> >
> > ---
> > +static char *
> > +wait_for_valid_params_and_get_dbname(void)
> > +{
> > +   char       *dbname;
> > +   int         rc;
> > +
> > +   /* Sanity check. */
> > +   Assert(enable_syncslot);
> > +
> > +   for (;;)
> > +   {
> > +       if (validate_parameters_and_get_dbname(&dbname))
> > +           break;
> > +       ereport(LOG, errmsg("skipping slot synchronization"));
> > +
> > +       ProcessSlotSyncInterrupts(NULL);
> >
> > When reading this function, I expected that the slotsync worker would
> > resume working once the parameters became valid, but it was not
> > correct. For example, if I changed hot_standby_feedback from off to
> > on, the slotsync worker reads the config file, exits, and then
> > restarts. Given that the slotsync worker ends up exiting on parameter
> > changes anyway, why do we want to have it wait for parameters to
> > become valid?
> >
>
> Right, the reason for waiting is to avoid repeated re-start of
> slotsync worker if the required parameter is not changed. To follow
> that, I think we should simply continue when the required parameter is
> changed and is valid. But, I think during actual slotsync, if
> connection_info is changed then there is no option but to restart.

Agreed.

> >
> > ---
> > +bool
> > +SlotSyncWorkerCanRestart(void)
> > +{
> > +#define SLOTSYNC_RESTART_INTERVAL_SEC 10
> > +
> >
> > IIUC depending on how busy the postmaster is and the timing, the user
> > could wait for 1 min to re-launch the slotsync worker. But I think the
> > user might want to re-launch the slotsync worker more quickly for
> > example when the slotsync worker restarts due to parameter changes.
> > IIUC SloSyncWorkerCanRestart() doesn't consider the fact that the
> > slotsync worker previously exited with 0 or 1.
> >
>
> Considering my previous where we don't want to restart for a required
> parameter change, isn't it better to avoid repeated restart (say when
> the user gave an invalid dbname)? BTW, I think this restart interval
> is added based on your previous complaint [1].

I think it's useful that the slotsync worker restarts immediately when
a required parameter is changed but waits to restart when it exits
with an error. IIUC the apply worker does so; if it restarts due to a
subscription parameter change, it resets the last-start time so that
the launcher will restart it without waiting. But if it exits with an
error, the launcher waits for wal_retrieve_retry_interval. I don't
think the slotsync worker must follow this behavior but I feel it's
useful behavior.

>
> >
> > ---
> > When I dropped a database on the primary that has a failover slot, I
> > got the following logs on the standby:
> >
> > 2024-01-31 17:25:21.750 JST [1103933] FATAL:  replication slot "s" is
> > active for PID 1103935
> > 2024-01-31 17:25:21.750 JST [1103933] CONTEXT:  WAL redo at 0/3020D20
> > for Database/DROP: dir 1663/16384
> > 2024-01-31 17:25:21.751 JST [1103930] LOG:  startup process (PID
> > 1103933) exited with exit code 1
> >
> > It seems that because the slotsync worker created the slot on the
> > standby, the slot's active_pid is still valid.
> >
>
> But we release the slot after sync. And we do take a shared lock on
> the database to make the startup process wait for slotsync. There is
> one gap which is that we don't reset active_pid for temp slots in
> ReplicationSlotRelease(), so for temp slots such an error can occur
> but OTOH, we immediately make the slot persistent after sync. As per
> my understanding, it is only possible to get this error if the initial
> sync doesn't happen and the slot remains temporary. Is that your case?
> How did reproduce this?

I created a failover slot manually on the primary and dropped the
database where the failover slot is created. So this would not happen
in normal cases.

BTW I've tested the following switch/fail-back scenario but it seems
not to work fine. Am I missing something?

Setup:
node1 is the primary, node2 is the physical standby for node1, and
node3 is the subscriber connecting to node1.

Steps:
1. [node1]: create a table and a publication for the table.
2. [node2]: set enable_syncslot = on and start (to receive WALs from node1).
3. [node3]: create a subscription with failover = true for the publication.
4. [node2]: promote to the new standby.
5. [node3]: alter subscription to connect the new primary, node2.
6. [node1]: stop, set enable_syncslot = on (and other required
parameters), then start as a new standby.

Then I got the error "exiting from slot synchronization because same
name slot "test_sub" already exists on the standby".

The logical replication slot that was created on the old primary
(node1) has been synchronized to the old standby (node2). Therefore on
node2, the slot's "synced" field is true. However, once node1 starts
as the new standby with slot synchronization, the slotsync worker
cannot synchronize the slot because the slot's "synced" field on the
primary is false.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





^ permalink  raw  reply  [nested|flat] 119+ messages in thread


end of thread, other threads:[~2024-01-31 15:49 UTC | newest]

Thread overview: 119+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-12-30 21:23 Synchronizing slots from primary to standby Petr Jelinek <[email protected]>
2019-07-08 10:28 ` Thomas Munro <[email protected]>
2019-03-28 23:50 [PATCH v3 01/12] review docs for pg12dev Justin Pryzby <[email protected]>
2021-10-31 10:08 Synchronizing slots from primary to standby Peter Eisentraut <[email protected]>
2021-11-24 06:11 ` Masahiko Sawada <[email protected]>
2021-12-14 22:13   ` Peter Eisentraut <[email protected]>
2021-11-24 16:25 ` Dimitri Fontaine <[email protected]>
2021-12-14 22:19   ` Peter Eisentraut <[email protected]>
2021-11-28 06:52 ` Bharath Rupireddy <[email protected]>
2021-11-28 20:17   ` SATYANARAYANA NARLAPURAM <[email protected]>
2021-11-29 04:09     ` Bharath Rupireddy <[email protected]>
2021-11-29 05:44       ` Dilip Kumar <[email protected]>
2021-11-29 06:49         ` Bharath Rupireddy <[email protected]>
2021-11-29 07:39           ` Dilip Kumar <[email protected]>
2021-11-29 12:28             ` Bharath Rupireddy <[email protected]>
2021-12-14 22:24   ` Peter Eisentraut <[email protected]>
2021-12-16 02:15     ` Hsu, John <[email protected]>
2021-12-14 22:12 ` Peter Eisentraut <[email protected]>
2022-11-15 09:02   ` Drouvot, Bertrand <[email protected]>
2024-01-12 12:19 ` shveta malik <[email protected]>
2024-01-13 07:23   ` Amit Kapila <[email protected]>
2024-01-16 03:33     ` shveta malik <[email protected]>
2024-01-16 04:07       ` Amit Kapila <[email protected]>
2024-01-16 04:33         ` Dilip Kumar <[email protected]>
2024-01-16 07:28         ` Masahiko Sawada <[email protected]>
2024-01-16 09:40           ` shveta malik <[email protected]>
2024-01-16 11:57             ` shveta malik <[email protected]>
2024-01-17 00:42               ` Peter Smith <[email protected]>
2024-01-17 04:33               ` Peter Smith <[email protected]>
2024-01-17 04:37               ` Peter Smith <[email protected]>
2024-01-17 09:38               ` Bertrand Drouvot <[email protected]>
2024-01-17 10:30                 ` shveta malik <[email protected]>
2024-01-18 05:01                   ` Peter Smith <[email protected]>
2024-01-19 03:41                     ` shveta malik <[email protected]>
2024-01-18 11:19                   ` Amit Kapila <[email protected]>
2024-01-19 10:55                     ` shveta malik <[email protected]>
2024-01-18 12:55                   ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-19 05:05                   ` Masahiko Sawada <[email protected]>
2024-01-19 10:25                     ` shveta malik <[email protected]>
2024-01-22 06:58                       ` Amit Kapila <[email protected]>
2024-01-22 11:33                         ` shveta malik <[email protected]>
2024-01-24 05:10                         ` Masahiko Sawada <[email protected]>
2024-01-24 05:43                           ` Amit Kapila <[email protected]>
2024-01-24 05:53                             ` Masahiko Sawada <[email protected]>
2024-01-24 08:21                               ` Amit Kapila <[email protected]>
2024-01-24 09:08                                 ` Bertrand Drouvot <[email protected]>
2024-01-24 10:39                                   ` shveta malik <[email protected]>
2024-01-24 11:47                                     ` shveta malik <[email protected]>
2024-01-25 01:37                                       ` Peter Smith <[email protected]>
2024-01-25 03:43                                       ` Amit Kapila <[email protected]>
2024-01-25 06:11                                         ` shveta malik <[email protected]>
2024-01-25 05:08                                       ` Peter Smith <[email protected]>
2024-01-25 11:47                                         ` shveta malik <[email protected]>
2024-01-25 05:56                                     ` Bertrand Drouvot <[email protected]>
2024-01-25 04:24                           ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-19 06:18                   ` Peter Smith <[email protected]>
2024-01-22 11:37                     ` shveta malik <[email protected]>
2024-01-19 11:53                   ` Amit Kapila <[email protected]>
2024-01-20 05:21                     ` Dilip Kumar <[email protected]>
2024-01-20 10:44                       ` Amit Kapila <[email protected]>
2024-01-22 11:58                         ` Masahiko Sawada <[email protected]>
2024-01-22 12:26                           ` Amit Kapila <[email protected]>
2024-01-22 15:12                             ` Masahiko Sawada <[email protected]>
2024-01-23 05:39                               ` Amit Kapila <[email protected]>
2024-01-22 04:19                     ` Amit Kapila <[email protected]>
2024-01-22 07:41                     ` Bertrand Drouvot <[email protected]>
2024-01-22 09:40                       ` Amit Kapila <[email protected]>
2024-01-22 11:30                         ` shveta malik <[email protected]>
2024-01-23 04:14                           ` Peter Smith <[email protected]>
2024-01-23 11:50                             ` shveta malik <[email protected]>
2024-01-23 09:07                           ` Ajin Cherian <[email protected]>
2024-01-23 11:43                             ` shveta malik <[email protected]>
2024-01-24 03:21                               ` Peter Smith <[email protected]>
2024-01-24 04:09                                 ` Amit Kapila <[email protected]>
2024-01-24 10:30                               ` Amit Kapila <[email protected]>
2024-01-25 02:57                                 ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-25 07:55                                   ` Bertrand Drouvot <[email protected]>
2024-01-25 10:24                                     ` Amit Kapila <[email protected]>
2024-01-25 11:51                                       ` Bertrand Drouvot <[email protected]>
2024-01-25 13:11                                       ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-26 08:31                                         ` Bertrand Drouvot <[email protected]>
2024-01-27 03:43                                         ` Amit Kapila <[email protected]>
2024-01-27 06:31                                           ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-29 03:50                                             ` Peter Smith <[email protected]>
2024-01-29 04:05                                               ` Amit Kapila <[email protected]>
2024-01-29 09:41                                                 ` shveta malik <[email protected]>
2024-01-29 11:30                                                   ` Amit Kapila <[email protected]>
2024-01-29 13:17                                                     ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-29 13:47                                                       ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-30 01:59                                                       ` Peter Smith <[email protected]>
2024-01-30 04:06                                                         ` Amit Kapila <[email protected]>
2024-01-30 06:01                                                       ` Amit Kapila <[email protected]>
2024-01-30 10:36                                                         ` shveta malik <[email protected]>
2024-01-30 12:52                                                           ` shveta malik <[email protected]>
2024-01-31 08:32                                                             ` Masahiko Sawada <[email protected]>
2024-01-31 10:42                                                               ` Amit Kapila <[email protected]>
2024-01-31 15:49                                                                 ` Masahiko Sawada <[email protected]>
2024-01-31 01:57                                                           ` Peter Smith <[email protected]>
2024-01-31 03:18                                                             ` Amit Kapila <[email protected]>
2024-01-31 05:10                                                               ` Peter Smith <[email protected]>
2024-01-31 07:23                                                             ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-29 04:54                                             ` shveta malik <[email protected]>
2024-01-29 08:52                                               ` Bertrand Drouvot <[email protected]>
2024-01-29 09:05                                                 ` Amit Kapila <[email protected]>
2024-01-29 09:15                                                   ` Bertrand Drouvot <[email protected]>
2024-01-30 15:45                                                     ` Bertrand Drouvot <[email protected]>
2024-01-17 10:36                 ` shveta malik <[email protected]>
2024-01-19 05:52                   ` Amit Kapila <[email protected]>
2024-01-19 06:16                     ` shveta malik <[email protected]>
2024-01-19 08:12                       ` Bertrand Drouvot <[email protected]>
2024-01-19 10:48                         ` shveta malik <[email protected]>
2024-01-22 03:36                           ` shveta malik <[email protected]>
2024-01-22 08:49                             ` Zhijie Hou (Fujitsu) <[email protected]>
2024-01-17 01:13             ` Masahiko Sawada <[email protected]>
2024-01-17 04:48               ` shveta malik <[email protected]>
2024-01-17 05:11               ` Nisha Moond <[email protected]>
2024-01-16 08:42     ` Bertrand Drouvot <[email protected]>
2024-01-16 12:02     ` shveta malik <[email protected]>
2024-01-18 23:11 ` Peter Smith <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox